@loompad/cli 1.2.2 → 1.2.4

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 (49) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +10 -0
  3. package/dist/cli/index.js +59 -7
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/core/crew.d.ts +16 -1
  6. package/dist/core/crew.js +26 -2
  7. package/dist/core/crew.js.map +1 -1
  8. package/dist/core/orchestra.d.ts +36 -0
  9. package/dist/core/orchestra.js +154 -82
  10. package/dist/core/orchestra.js.map +1 -1
  11. package/dist/core/team-hub.d.ts +2 -1
  12. package/dist/core/team-hub.js +1 -1
  13. package/dist/core/team-hub.js.map +1 -1
  14. package/dist/core/team-leases.d.ts +7 -0
  15. package/dist/core/team-leases.js +19 -0
  16. package/dist/core/team-leases.js.map +1 -1
  17. package/dist/daemon/client.d.ts +12 -1
  18. package/dist/daemon/client.js +5 -2
  19. package/dist/daemon/client.js.map +1 -1
  20. package/dist/daemon/routes/brain.js +2 -2
  21. package/dist/daemon/routes/brain.js.map +1 -1
  22. package/dist/daemon/routes/orchestra.js +1 -0
  23. package/dist/daemon/routes/orchestra.js.map +1 -1
  24. package/dist/daemon/routes/project-team.js +17 -0
  25. package/dist/daemon/routes/project-team.js.map +1 -1
  26. package/dist/daemon/runner.d.ts +11 -0
  27. package/dist/daemon/runner.js +111 -5
  28. package/dist/daemon/runner.js.map +1 -1
  29. package/dist/daemon/runtime/briefings.js +12 -1
  30. package/dist/daemon/runtime/briefings.js.map +1 -1
  31. package/dist/daemon/runtime.d.ts +9 -2
  32. package/dist/daemon/runtime.js +8 -4
  33. package/dist/daemon/runtime.js.map +1 -1
  34. package/dist/daemon/team-coordinator.d.ts +27 -3
  35. package/dist/daemon/team-coordinator.js +54 -2
  36. package/dist/daemon/team-coordinator.js.map +1 -1
  37. package/dist/daemon/team.d.ts +42 -1
  38. package/dist/daemon/team.js +92 -1
  39. package/dist/daemon/team.js.map +1 -1
  40. package/dist/hub/supabase-client.d.ts +4 -2
  41. package/dist/hub/supabase-client.js +21 -12
  42. package/dist/hub/supabase-client.js.map +1 -1
  43. package/dist/web/app.css +12 -0
  44. package/dist/web/app.js +140 -28
  45. package/dist/web/app.js.map +3 -3
  46. package/docs/release-notes/v1.2.3.md +29 -0
  47. package/docs/release-notes/v1.2.4.md +13 -0
  48. package/docs/teams.md +31 -0
  49. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.4] — 2026-10-22
11
+
12
+ Team auto-orchestration, set up in one step and steadier on a teammate's machine.
13
+
14
+ ### Added
15
+
16
+ - **`loom team auto` — two machines, one goal, set up in one step.** `loom team auto` says whether team auto-orchestration is ready in this project, which teammates' agents a goal would get, and what's missing; `loom team auto on` shares the project, writes `loom.team.json` with `runners.shared` when the repo has none (commit and merge it), and turns on this machine's shared runner. The Team tab shows the same as a **Two machines, one goal** card with a **Turn on** button.
17
+
18
+ ### Fixed
19
+
20
+ - **A teammate's task could still fail to commit on their machine.** The clone's own Loom takes its end-of-turn checkpoint in the git index at the same moment the task commits, and could undo the task's staging ("nothing to commit"). The runner now builds the task's commit in a private index, so nothing else in the clone can disturb it.
21
+ - `loom crew status` in a project with several crews said "which crew?" instead of listing them; it lists every crew unless you name one.
22
+ - **A follow-up to a teammate's task re-ran the original prompt.** When the orchestrator sent a follow-up ("also fix the test"), the teammate's machine got the task from scratch. It now gets the follow-up, on top of the first attempt's work, with the original task for context.
23
+
24
+ ## [1.2.3] — 2026-10-22
25
+
26
+ Teams that work together on their own: one goal splits across teammates' machines. Plus Simple mode, hub hardening, and the fourth field round.
27
+
28
+ ### Added
29
+
30
+ - **Team auto-orchestration: two people, one goal, both machines.** With a teammate's Loom running as a shared runner (`loom runner start --shared`, and `runners.shared` in the repo's `loom.team.json`), the goal you start splits across both of you: your orchestrator gets their agents as `team:<agent>` workers, a task sent to one is claimed by their runner on its own and run with *their* agent in a fresh clone, and its commits come back through origin into your goal's integration branch — still one PR, from your machine. `loom orchestrate --solo` keeps a goal local. A task no teammate's machine claims within 10 minutes goes back to the orchestrator to hand to someone else. Hub jobs gain a `task` kind (migration 0012).
31
+ - **Simple mode, and it's the default.** A project opens on five plain tabs — Chat, Team, Agents, Board, Memory — and a composer that's just a box, the agent and Plan. No Orchestra or Crew tabs, no Insights metrics, no terminal, console, browser or right-hand panel, no model, permission or context chips, no cost meter or baton in the status bar, no MCP/skills menu; ⌘K and the menu shortcuts leave those out too, and a stray link to one says where it is instead of half-opening it.
32
+ - **Developer mode** (Settings → Preferences → Mode) turns every one of those back on, instantly and per device. Nothing was removed — it's all where it was.
33
+ - `loom memory:repair --dry-run` shows each memory it would change, before → after, and writes nothing (#326).
34
+ - `loom crew status --json`: crews, goals and every card with its id, stage, touches and hold (#329).
35
+
36
+ ### Security
37
+
38
+ - **Hub hardening (migration 0012).** Presence rows get an opaque key, so a disconnect no longer broadcasts another team's repo names to every signed-in subscriber (Realtime skips RLS on deletes); a member can no longer overwrite a teammate's key envelope; the membership helpers aren't callable by anonymous users; stale presence, expired leases and invites, and feed older than 90 days are pruned hourly.
39
+
40
+ ### Fixed
41
+
42
+ - **`loom runner stop` really stops taking work.** A stopped runner still claimed a job the hub pushed to it; it now takes nothing new until it's started again.
43
+ - Simple mode no longer shows Developer vocabulary: the chat's handoff chip drops the word "baton" (and the opening hand-out from nobody), and the Agents tab hides the baton and permission badges.
44
+ - The Verified card quoted an agent's claim with its raw `**`/backticks; it quotes the words.
45
+ - A workspace tab could fail to switch when the window wasn't painting (backgrounded), because its crossfade never ran; the click now switches it regardless.
46
+ - **`memory:repair` now shows in `loom memory` and briefings** (#325). A decision is kept twice: an event (the record) and a memory (the live copy). The repair fixed the memory, but the Memory view and every handoff briefing read the record, so the old text stayed. They now read the live copy.
47
+ - **A crew card no longer loops on its own lease** (#327). Codex reports edits by absolute path inside the card's worktree, and leases are repo-relative globs, so every edit looked out of bounds. The card re-held within a minute of each `crew overlap`, and the builder never ran a turn. Paths are now made repo-relative before matching.
48
+ - **Crew checkouts follow orchestra's** (#328). A project on another disk keeps them in `.loom-worktrees` beside it, not on the internal disk; `orchestra.worktreeDir` decides for both. A checkout that already exists stays where it is.
49
+ - **`crew status` shows card ids** (#329), plus a hint per failed card (`loom crew accept --card <id>`). A lease hold now points at `crew overlap` rather than `crew say`. The goal's `failed` event lists `failedCards`.
50
+
51
+
10
52
  ## [1.2.2] — 2026-10-18
11
53
 
12
54
  The Team tab and GitHub sign-in, and everything a third stress test (5 projects at once, restarts mid-run) found.
package/README.md CHANGED
@@ -1560,6 +1560,16 @@ loom team # who's doing what, right now
1560
1560
  - **Worker commits carry `Loom-Goal`, `Loom-Task`, `Loom-Agent` and `Loom-Member`
1561
1561
  trailers.** `git log` alone tells you which goal, task and agent wrote a line.
1562
1562
 
1563
+ **Two people, one goal — the agents split it themselves.** When teammates turn
1564
+ on team auto-orchestration (`loom team auto on`, or **Turn on** in the Team tab),
1565
+ a goal you start splits across both machines. Your orchestrator gets their agents
1566
+ as `team:codex`, `team:opencode`…; a task sent to one is claimed by their Loom on
1567
+ its own, run with **their** agent and subscription, and merged back into your
1568
+ goal's branch — still one PR. A teammate who goes offline doesn't stall you: an
1569
+ unclaimed task returns to your orchestrator after 10 minutes. `loom team auto`
1570
+ says what's missing; `loom orchestrate "…" --solo` keeps a goal on your machine.
1571
+ See [docs/teams.md](docs/teams.md#two-people-one-goal-team-auto-orchestration).
1572
+
1563
1573
  **Phase 2 (stop colliding) is built too:**
1564
1574
  - **Declaring touches.** On a team project, every orchestra task declares the files
1565
1575
  it will `touch`, and Loom takes a lease on them.
package/dist/cli/index.js CHANGED
@@ -1506,6 +1506,7 @@ program
1506
1506
  .option("--plan", "plan mode: write the plan as markdown specs under plans/<run>/ that any agent can pick up")
1507
1507
  .option("--max-usd <n>", "stop this goal when it has spent this much, and say so")
1508
1508
  .option("--race", "race: every worker takes the whole goal in its own worktree; pick one with orchestra:apply <run> --task race-<agent>")
1509
+ .option("--solo", "keep every task on this machine, even when teammates' runners could take some")
1509
1510
  .option("--no-watch", "start it and return instead of following it to the end")
1510
1511
  .action(async (goal, opts) => {
1511
1512
  const client = await ensureDaemon();
@@ -1520,7 +1521,11 @@ program
1520
1521
  ...(opts.plan ? { plan: true } : {}),
1521
1522
  ...(opts.maxUsd ? { maxUsd: Number(opts.maxUsd) } : {}),
1522
1523
  ...(opts.race ? { race: true } : {}),
1524
+ ...(opts.solo ? { team: false } : {}),
1523
1525
  });
1526
+ const mates = run.workers.filter((w) => w.startsWith("team:"));
1527
+ if (mates.length)
1528
+ console.log(pc.dim(` teammates' machines can take tasks too: ${mates.join(", ")}`));
1524
1529
  console.log(run.race
1525
1530
  ? `${pc.magenta("🏁")} ${pc.bold(run.id)} started — ${run.workers.join(", ")} racing on the same goal`
1526
1531
  : `${pc.magenta("🎼")} ${pc.bold(run.id)} started — ${pc.bold(run.orchestrator.agent)} orchestrating ` +
@@ -1855,12 +1860,23 @@ program
1855
1860
  .option("--card <id>", "overlap: held card id; accept: the failed card to take as it stands")
1856
1861
  .option("--decision <decision>", "overlap: narrow, wait:<goal>, or proceed:<reason>")
1857
1862
  .option("--touches <globs>", "overlap: comma-separated revised paths")
1863
+ .option("--json", "status: machine-readable")
1858
1864
  .action(async (action, args, opts) => {
1859
1865
  const client = await ensureDaemon();
1860
1866
  const project = await currentProject(client);
1861
1867
  const a = (action ?? "status").toLowerCase();
1862
1868
  const words = (args ?? []).join(" ").trim();
1863
1869
  const { crews, templates } = await client.crews(project.id);
1870
+ if (a === "status" && opts.json) {
1871
+ // every crew, its goal and each card with its id — what accept/overlap take (#329)
1872
+ const pick = opts.crew ? crews.filter((c) => c.id === opts.crew || c.name === opts.crew) : crews;
1873
+ console.log(JSON.stringify({ crews: pick.map((c) => ({ id: c.id, name: c.name, busy: Boolean(c.busy), teammates: c.teammates,
1874
+ goal: c.state?.goal ? { id: c.state.goal.id, status: c.state.goal.status, text: c.state.goal.text, branch: c.state.goal.branch,
1875
+ question: c.state.goal.question ?? null, costUsd: c.state.goal.costUsd ?? null,
1876
+ cards: (c.state.goal.cards ?? []).map((k) => ({ id: k.id, title: k.title, stage: k.stage, builder: k.builder ?? null,
1877
+ touches: k.touches ?? [], blockedBy: k.blockedBy ?? [], error: k.error ?? null, hold: k.teamHold ?? k.hold ?? null })) } : null })) }, null, 2));
1878
+ return;
1879
+ }
1864
1880
  if (a === "create") {
1865
1881
  const [first, ...rest] = args ?? [];
1866
1882
  const template = first && templates.includes(first.toLowerCase()) ? first.toLowerCase() : "ship";
@@ -1878,12 +1894,17 @@ program
1878
1894
  console.log(pc.dim(`no crews here yet — loom crew create [${templates.join("|")}] [name]`));
1879
1895
  return;
1880
1896
  }
1897
+ // status shows every crew unless one is named; everything else needs to know which
1898
+ if (a === "status" && !opts.crew) {
1899
+ for (const c of crews)
1900
+ printCrew(c);
1901
+ return;
1902
+ }
1881
1903
  const crew = opts.crew ? crews.find((c) => c.id === opts.crew || c.name === opts.crew) : crews.length === 1 ? crews[0] : undefined;
1882
1904
  if (!crew)
1883
1905
  throw new Error(`which crew? --crew ${crews.map((c) => c.id).join(" | ")}`);
1884
1906
  if (a === "status") {
1885
- for (const c of opts.crew ? [crew] : crews)
1886
- printCrew(c);
1907
+ printCrew(crew);
1887
1908
  return;
1888
1909
  }
1889
1910
  if (a === "diff") {
@@ -1920,10 +1941,16 @@ function printCrew(c) {
1920
1941
  console.log(` ${color(g.status)} ${g.text.split("\n")[0].slice(0, 80)} ${pc.dim(g.branch)}`);
1921
1942
  for (const card of g.cards ?? []) {
1922
1943
  const m = card.stage === "done" ? pc.green("✓") : card.stage === "failed" ? pc.red("✗") : card.stage === "planned" ? pc.dim("·") : pc.cyan("→");
1923
- console.log(` ${m} ${card.title}${card.builder ? pc.dim(` ${card.builder}`) : ""}${card.stage !== "done" && card.stage !== "planned" ? pc.dim(` [${card.stage}]`) : ""}${card.error ? pc.red(` ${card.error.slice(0, 80)}`) : ""}`);
1944
+ console.log(` ${m} ${pc.dim(card.id)} ${card.title}${card.builder ? pc.dim(` ${card.builder}`) : ""}${card.stage !== "done" && card.stage !== "planned" ? pc.dim(` [${card.stage}]`) : ""}${card.error ? pc.red(` ${card.error.slice(0, 80)}`) : ""}`);
1924
1945
  }
1946
+ // a lease hold is settled with overlap (widen or narrow the card's paths), not by answering it
1947
+ const leaseHold = (g.cards ?? []).find((k) => k.teamHold?.kind === "decide");
1925
1948
  if (g.question)
1926
- console.log(pc.yellow(` ${g.question.teammate} asks: ${g.question.text}`) + pc.dim(" (loom crew say \"…\")"));
1949
+ console.log(pc.yellow(` ${g.question.teammate} asks: ${g.question.text}`) + pc.dim(leaseHold
1950
+ ? ` (loom crew overlap --card ${leaseHold.id} --decision "proceed: <why>" --touches "<paths>")`
1951
+ : " (loom crew say \"…\")"));
1952
+ for (const k of (g.cards ?? []).filter((x) => x.stage === "failed"))
1953
+ console.log(pc.dim(` take ${k.id} as it stands: loom crew accept --card ${k.id}`));
1927
1954
  if (g.status === "awaiting_approval")
1928
1955
  console.log(pc.dim(" loom crew approve — or loom crew say \"…\" to change the plan"));
1929
1956
  if (g.status === "completed" && !g.applied)
@@ -1933,7 +1960,7 @@ function printCrew(c) {
1933
1960
  }
1934
1961
  program
1935
1962
  .command("team [action] [args...]")
1936
- .description("Loom Teams: status | signin [hub] | create <name> | invite | join <link> | share | unshare | brain [inbox|promote|resolve|correct|trust|private] | landing | doctor [fix] | adopt <pr> | deploys | release-notes <since> | webhook [--repo r] [--install] | remove <github> | leave")
1963
+ .description("Loom Teams: status | auto [on] | signin [hub] | create <name> | invite | join <link> | share | unshare | brain [inbox|promote|resolve|correct|trust|private] | landing | doctor [fix] | adopt <pr> | deploys | release-notes <since> | webhook [--repo r] [--install] | remove <github> | leave")
1937
1964
  .option("--github <login>", "your GitHub login (defaults to the gh CLI's)")
1938
1965
  .option("--repo <owner/name>", "webhook: which repo (defaults to this project's)")
1939
1966
  .option("--install", "webhook: create it on the repo with gh (needs repo admin)")
@@ -1949,6 +1976,24 @@ program
1949
1976
  try {
1950
1977
  if (a === "status")
1951
1978
  return void printTeam(await client.team());
1979
+ if (a === "auto") {
1980
+ // team auto-orchestration: your goals split across teammates' machines, theirs across yours
1981
+ const project = await currentProject(client);
1982
+ const st = await client.teamAuto(project.id, arg === "on");
1983
+ if (st.wrotePolicy)
1984
+ console.log(`${pc.green("✓")} wrote loom.team.json (runners.shared) — commit and merge it; policy is reviewed like code`);
1985
+ const mark = (ok) => (ok ? pc.green("✓") : pc.dim("·"));
1986
+ console.log(`${mark(st.shared)} project shared with your team`);
1987
+ console.log(`${mark(st.policyShared)} repo policy lets teammates' runners take its work`);
1988
+ console.log(`${mark(st.myRunner.running && st.myRunner.shared)} your runner is on and shared (teammates' goals can use your agents)`);
1989
+ const on = st.teammates.filter((t) => t.online);
1990
+ console.log(`${mark(on.length > 0)} teammates' runners online: ${on.length ? on.map((t) => `${t.github} (${t.kinds.join(", ")})`).join(", ") : "none yet"}`);
1991
+ if (st.ready)
1992
+ console.log(`\n${pc.green("ready")} — goals you start get ${st.workers.join(", ")} as workers. ${pc.dim("loom orchestrate \"…\" (--solo to keep one local)")}`);
1993
+ else
1994
+ console.log(`\n${pc.yellow("not yet")} — ${st.missing.join("; ")}`);
1995
+ return;
1996
+ }
1952
1997
  if (a === "signin") {
1953
1998
  const { hostedHubUrl, hostedSupabaseUrl, publishableKeyFor } = await import("../core/hosted.js");
1954
1999
  const supabaseUrl = hostedSupabaseUrl(arg);
@@ -3166,10 +3211,17 @@ program
3166
3211
  program
3167
3212
  .command("memory:repair")
3168
3213
  .description("mend decisions recorded before 1.2.1 whose underscores and paths were stripped, from the planner's own words")
3169
- .action(async () => {
3214
+ .option("--dry-run", "show what would change, and change nothing")
3215
+ .action(async (opts) => {
3170
3216
  const client = await ensureDaemon();
3171
3217
  const project = await currentProject(client);
3172
- const { repaired, checked } = await client.repairDecisions(project.id);
3218
+ const { repaired, checked, changes } = await client.repairDecisions(project.id, { dryRun: opts.dryRun });
3219
+ if (opts.dryRun) {
3220
+ for (const c of changes ?? [])
3221
+ console.log(`${pc.dim(c.id)}\n ${pc.red("- " + c.before)}\n ${pc.green("+ " + c.after)}`);
3222
+ console.log(repaired ? pc.yellow(`would mend ${repaired} of ${checked} review decisions — nothing written (run without --dry-run to apply)`) : pc.dim(`nothing to mend among ${checked} review decisions`));
3223
+ return;
3224
+ }
3173
3225
  console.log(repaired ? pc.green(`✓ mended ${repaired} of ${checked} review decisions`) : pc.dim(`nothing to mend among ${checked} review decisions`));
3174
3226
  });
3175
3227
  program