@loompad/cli 1.2.2 → 1.2.3

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 (45) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/cli/index.js +33 -4
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/core/crew.d.ts +16 -1
  5. package/dist/core/crew.js +26 -2
  6. package/dist/core/crew.js.map +1 -1
  7. package/dist/core/orchestra.d.ts +36 -0
  8. package/dist/core/orchestra.js +149 -82
  9. package/dist/core/orchestra.js.map +1 -1
  10. package/dist/core/team-hub.d.ts +2 -1
  11. package/dist/core/team-hub.js +1 -1
  12. package/dist/core/team-hub.js.map +1 -1
  13. package/dist/core/team-leases.d.ts +7 -0
  14. package/dist/core/team-leases.js +19 -0
  15. package/dist/core/team-leases.js.map +1 -1
  16. package/dist/daemon/client.d.ts +9 -1
  17. package/dist/daemon/client.js +2 -2
  18. package/dist/daemon/client.js.map +1 -1
  19. package/dist/daemon/routes/brain.js +2 -2
  20. package/dist/daemon/routes/brain.js.map +1 -1
  21. package/dist/daemon/routes/orchestra.js +1 -0
  22. package/dist/daemon/routes/orchestra.js.map +1 -1
  23. package/dist/daemon/runner.d.ts +11 -0
  24. package/dist/daemon/runner.js +96 -3
  25. package/dist/daemon/runner.js.map +1 -1
  26. package/dist/daemon/runtime/briefings.js +12 -1
  27. package/dist/daemon/runtime/briefings.js.map +1 -1
  28. package/dist/daemon/runtime.d.ts +9 -2
  29. package/dist/daemon/runtime.js +8 -4
  30. package/dist/daemon/runtime.js.map +1 -1
  31. package/dist/daemon/team-coordinator.d.ts +27 -3
  32. package/dist/daemon/team-coordinator.js +54 -2
  33. package/dist/daemon/team-coordinator.js.map +1 -1
  34. package/dist/daemon/team.d.ts +9 -1
  35. package/dist/daemon/team.js +44 -1
  36. package/dist/daemon/team.js.map +1 -1
  37. package/dist/hub/supabase-client.d.ts +4 -2
  38. package/dist/hub/supabase-client.js +21 -12
  39. package/dist/hub/supabase-client.js.map +1 -1
  40. package/dist/web/app.css +4 -0
  41. package/dist/web/app.js +96 -27
  42. package/dist/web/app.js.map +3 -3
  43. package/docs/release-notes/v1.2.3.md +29 -0
  44. package/docs/teams.md +28 -0
  45. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.3] — 2026-10-22
11
+
12
+ Teams that work together on their own: one goal splits across teammates' machines. Plus Simple mode, hub hardening, and the fourth field round.
13
+
14
+ ### Added
15
+
16
+ - **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).
17
+ - **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.
18
+ - **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.
19
+ - `loom memory:repair --dry-run` shows each memory it would change, before → after, and writes nothing (#326).
20
+ - `loom crew status --json`: crews, goals and every card with its id, stage, touches and hold (#329).
21
+
22
+ ### Security
23
+
24
+ - **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.
25
+
26
+ ### Fixed
27
+
28
+ - **`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.
29
+ - 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.
30
+ - The Verified card quoted an agent's claim with its raw `**`/backticks; it quotes the words.
31
+ - 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.
32
+ - **`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.
33
+ - **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.
34
+ - **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.
35
+ - **`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`.
36
+
37
+
10
38
  ## [1.2.2] — 2026-10-18
11
39
 
12
40
  The Team tab and GitHub sign-in, and everything a third stress test (5 projects at once, restarts mid-run) found.
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";
@@ -1920,10 +1936,16 @@ function printCrew(c) {
1920
1936
  console.log(` ${color(g.status)} ${g.text.split("\n")[0].slice(0, 80)} ${pc.dim(g.branch)}`);
1921
1937
  for (const card of g.cards ?? []) {
1922
1938
  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)}`) : ""}`);
1939
+ 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
1940
  }
1941
+ // a lease hold is settled with overlap (widen or narrow the card's paths), not by answering it
1942
+ const leaseHold = (g.cards ?? []).find((k) => k.teamHold?.kind === "decide");
1925
1943
  if (g.question)
1926
- console.log(pc.yellow(` ${g.question.teammate} asks: ${g.question.text}`) + pc.dim(" (loom crew say \"…\")"));
1944
+ console.log(pc.yellow(` ${g.question.teammate} asks: ${g.question.text}`) + pc.dim(leaseHold
1945
+ ? ` (loom crew overlap --card ${leaseHold.id} --decision "proceed: <why>" --touches "<paths>")`
1946
+ : " (loom crew say \"…\")"));
1947
+ for (const k of (g.cards ?? []).filter((x) => x.stage === "failed"))
1948
+ console.log(pc.dim(` take ${k.id} as it stands: loom crew accept --card ${k.id}`));
1927
1949
  if (g.status === "awaiting_approval")
1928
1950
  console.log(pc.dim(" loom crew approve — or loom crew say \"…\" to change the plan"));
1929
1951
  if (g.status === "completed" && !g.applied)
@@ -3166,10 +3188,17 @@ program
3166
3188
  program
3167
3189
  .command("memory:repair")
3168
3190
  .description("mend decisions recorded before 1.2.1 whose underscores and paths were stripped, from the planner's own words")
3169
- .action(async () => {
3191
+ .option("--dry-run", "show what would change, and change nothing")
3192
+ .action(async (opts) => {
3170
3193
  const client = await ensureDaemon();
3171
3194
  const project = await currentProject(client);
3172
- const { repaired, checked } = await client.repairDecisions(project.id);
3195
+ const { repaired, checked, changes } = await client.repairDecisions(project.id, { dryRun: opts.dryRun });
3196
+ if (opts.dryRun) {
3197
+ for (const c of changes ?? [])
3198
+ console.log(`${pc.dim(c.id)}\n ${pc.red("- " + c.before)}\n ${pc.green("+ " + c.after)}`);
3199
+ 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`));
3200
+ return;
3201
+ }
3173
3202
  console.log(repaired ? pc.green(`✓ mended ${repaired} of ${checked} review decisions`) : pc.dim(`nothing to mend among ${checked} review decisions`));
3174
3203
  });
3175
3204
  program