loom-agent 1.3.3 → 1.6.0

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 CHANGED
@@ -4,7 +4,130 @@ All notable changes to **Loom Code** are documented here.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased] — completion harness, specialist agents, headless browser, TUI refresh
7
+ ## [Unreleased]
8
+
9
+ ## [1.6.0] — 2026-10-17
10
+
11
+ ### Added
12
+ - **Transcript undo / redo (`/undo`, `/redo`, `/undo-files`).** The
13
+ conversation-level counterpart to `/restore` (which rewinds the *file
14
+ tree*): each user turn now pushes a full transcript snapshot — UI messages,
15
+ session messages, and todos — onto a bounded stack (last 50 turns). `/undo`
16
+ steps the transcript back one turn and `/redo` steps forward again. When the
17
+ undone turn also created a restore point, `/undo-files` rolls the *working
18
+ tree* back to that same point so files match the rewound conversation. The
19
+ engine (`src/core/transcript-undo.js`) is a pure, unit-tested stack that
20
+ snapshots-and-clones so later mutation can't corrupt stored state, clears
21
+ the redo branch on a new action (like any editor), and returns the step
22
+ *reference* so a restore-point id created in a background `setTimeout` can
23
+ be stitched onto it without relying on a fragile stack index. Wired into
24
+ `processSlash` and registered in `SLASH_LIST`; the existing `<leader>u` /
25
+ `<leader>r` keybinds now resolve to working commands instead of falling
26
+ through to "Unknown command". Undo/redo persist via the normal
27
+ `saveSession` path, so a rewind survives restart.
28
+ - **Startup update check.** `updateCheck()` (previously a no-op stub) now
29
+ probes the npm registry once every 24 hours with a 4s timeout and, when a
30
+ strictly newer `loom-agent` is published, prints a single stderr notice
31
+ (`New version available: 1.5.0 → 1.6.0` + the upgrade command) before the
32
+ TUI starts. Non-blocking and failure-silent by design: offline, slow, or
33
+ malformed responses never delay startup, the throttle stamp lives in
34
+ `~/.loom/update-check.json`, and `LOOM_NO_UPDATE_CHECK=1` disables it
35
+ entirely. Version comparison is a small tuple compare (no new dependency).
36
+ Unit-tested (`update.test.js`, registered in `test:unit`).
37
+ - **`/export` — transcript as markdown.** Writes the conversation (roles,
38
+ message text, tool rows, error flags) to `.loom/exports/<session>.md`, the
39
+ plain-text counterpart to `/share` (HTML). Bound to the existing
40
+ `<leader>x` keybind, which previously resolved to a slash command with no
41
+ handler.
42
+
43
+ ### Fixed
44
+ - **Model catalogs verified against live provider APIs.** Every model ID the
45
+ picker ships was checked against the authoritative source (OpenAI,
46
+ NVIDIA, OpenRouter and Groq `/v1/models`; Anthropic's official pricing +
47
+ deprecation pages; the models.dev cache for registry providers), and every
48
+ dead ID was removed or repointed:
49
+ - **NVIDIA** (the reported failure): the default active model
50
+ `nvidia/llama-3.3-nemotron-super-49b-v1.5` did not exist and 404'd on
51
+ every request — repointed to `nvidia/nemotron-3-super-120b-a12b`
52
+ (54/54 catalog models live).
53
+ - **OpenAI:** dropped phantom `gpt-5.6`, `gpt-5.3-codex-spark`, `o3-pro`;
54
+ added live `gpt-5-codex`, `gpt-5.1-codex`, `gpt-5.1-codex-max`,
55
+ `gpt-5.1-codex-mini`, `gpt-5.2-codex`, `gpt-5.2-pro` (34/34 live).
56
+ - **Anthropic:** dropped models Anthropic retired in 2026
57
+ (`claude-sonnet-4-20250514`, `claude-opus-4-20250514`,
58
+ `claude-3-5-haiku-20241022`); added live `claude-fable-5`,
59
+ `claude-opus-4-7`, `claude-opus-4-6` (14 models, all prices matched to
60
+ the official pricing table; Mythos 5/5.1 omitted — verification-program
61
+ accounts only).
62
+ - **Defaults:** `settings.js` DEFAULTS and the shipped model map no longer
63
+ name dead IDs (`gpt-5-fast`, `meta/llama-3.1-8b-instruct`,
64
+ `anthropic/claude-sonnet-4`, `moonshotai/kimi-k3-free` → `gpt-5`,
65
+ `nemotron-3-super-120b-a12b`, `claude-sonnet-5-5`,
66
+ `z-ai/glm-5.3-free`); OpenRouter 42/42 and TokenRouter (models.dev's
67
+ single `z-ai/glm-5.3-free`) verified live.
68
+ - Usage/context tests now pin a deterministic model/window so future
69
+ default-model changes can't shift their expectations.
70
+ - **Speed telemetry now ticks during the thinking phase.** Reasoning deltas
71
+ count as first-token + generation tokens, so the sidebar latency gauge
72
+ starts when the model starts *thinking* instead of reading "waiting…"
73
+ through the entire thinking phase (measured on nemotron-3-super: ~7s to
74
+ the first thinking token, then +16s of thought before any visible text —
75
+ the response looked hung while the model was working). `last.latencyMs`
76
+ and `last.tokensPerSec` now reflect true time-to-first-model-output and
77
+ full generation throughput, including reasoning tokens. Regression test
78
+ in `session.test.js`.
79
+ - **NVIDIA catalog note: `/v1/models` is not account availability.** The
80
+ endpoint lists NVIDIA's global catalog, but chat POSTs can return
81
+ `Function '…': Not found for account '…'` when the key's account hasn't
82
+ enabled that model (probed 2026-10-17: 9/54 curated IDs answered chat on
83
+ one key — 37 account-gated 404s, ~6 hung to timeout, 2 returned 503).
84
+ Availability is per-account, so the curated list stays; the caveat is
85
+ now documented in `nvidia.js`.
86
+
87
+ ### Changed
88
+ - **Orchestrator now delegates by default.** The build agent's standing
89
+ instructions were rewritten to make fan-out the first move on any broad
90
+ SEARCH / ANALYSIS / OPTIMIZATION request ("find what causes X", "where is
91
+ Y", "how does Z work", "optimize W", "audit/investigate/locate"): it
92
+ splits the scope into independent questions and dispatches them as
93
+ **parallel `task` calls** (one `explore` subagent per disjoint area of the
94
+ codebase), then reads a file itself only to resolve a single ambiguous
95
+ line a subagent's report left open. This closes the gap where the agent
96
+ defaulted to reading files serially on its own. The plumbing already
97
+ existed (`task` tool, `runSubagent`, per-run cancellation, token rollup,
98
+ `Promise.all` execution) — this change makes using it the default rather
99
+ than an afterthought. Pinned by new assertions in `agents.test.js`.
100
+ - **Token optimization — prompt caching, cache-accurate pricing, and leaner
101
+ context (7 changes across the request path):**
102
+ - **OpenRouter/TokenRouter prompt caching:** the request body now sends
103
+ top-level `"cache_control": true`, so cache-capable models re-read the
104
+ cached prefix instead of re-paying full input cost each turn.
105
+ - **Cache-aware usage accounting + pricing:** `recordUsage` parses
106
+ `prompt_tokens_details.cached_tokens` / `cache_read_input_tokens` /
107
+ `cache_creation_input_tokens` / `cachedContentTokenCount` and prices
108
+ cached reads at **0.1×** and writes at **1.25×** the input rate instead
109
+ of the full rate. OpenAI/OpenRouter's cached count is treated as a
110
+ *subset* of the prompt total; Anthropic's is additive. Adds
111
+ `cacheReadTokens` / `cacheWriteTokens` session counters.
112
+ - **Cache-stable system prompt:** the per-turn skill block moved out of the
113
+ system string (injected into the last user message of a wire copy
114
+ instead), so the system prefix stays byte-identical across turns and
115
+ Anthropic's cache breakpoint keeps hitting even when the matched skill
116
+ changes.
117
+ - **Mode-specific system prompts:** chat and plan modes drop the skills
118
+ index, the todowrite mandate, and the tool-batching/behavior rules they
119
+ can't act on, cutting thousands of tokens per turn for tool-less modes.
120
+ - **Tool-result aging:** tool results older than the last 12 messages are
121
+ elided to a 2000/400 head/tail slice before they reach the provider
122
+ (kept verbatim in memory for the TUI), so a huge `read`/`grep` output
123
+ stops costing full input price every later call.
124
+ - **Slimmer tool schemas:** `mcp`, `task`, and `sandbox` descriptions
125
+ trimmed (package catalogs and per-agent rosters removed) — these ship in
126
+ every request's tool list.
127
+ - **Accurate `estimateTokens` / `/context`:** the estimate now includes the
128
+ per-turn agent/primary/sandbox/skill blocks and tool schemas, and the
129
+ `/context` command gained a `Schemas+turn` line so its total matches the
130
+ live window estimate.
8
131
 
9
132
  ### Security
10
133
  - **All dependency advisories cleared — `npm audit` reports 0
@@ -24,6 +147,85 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
24
147
  `npm audit fix`) — fixes the DoS/recursion advisories.
25
148
 
26
149
  ### Added
150
+ - **Full model-catalog refresh across every built-in provider (verified
151
+ 2026-10-17).** NVIDIA rebuilt from the live authenticated
152
+ `GET /v1/models` registry (81 live endpoints, chat-capable IDs only —
153
+ embeddings/guard/parse/video models excluded): **54 models** including
154
+ Nemotron 3.5 Lightning/Super VL, DeepSeek V4.1 Flash, GLM-5.3/Flash,
155
+ Gemma 4, DiffusionGemma, Kimi K2.6, Step 5 Preview, Palmyra (creative/
156
+ fin/med), Riva Translate and GPT-OSS 20B; 14 stale IDs (Llama 3.1/3.2/3.3
157
+ instruct, Nemotron Super 49B, GLM-5.2, GPT-OSS 120B, MiniMax M3, …) removed.
158
+ Anthropic 3 → **14 models** (Opus 5.5/5/4.8/4.5, Fable 5.1, Sonnet
159
+ 5.5/5/4.6/4.5, Haiku 5.5/4.5 + legacy dated snapshots). OpenAI 12 →
160
+ **31** (GPT-6 Astra/Sol/Luna, GPT-5.6/5.5/5.4 families incl. Pro and
161
+ Codex, o3/o4 legacy, GPT-4.1/4o). Google 7 → **15** (Gemini 4 Argon,
162
+ 3.1 Pro Preview, 3.8→3.5 Flash lines, latest-aliases, Gemma 4 31B/26B
163
+ A4B). OpenRouter 11 → **42** curated live slugs (Claude 5.5, GPT-6/5.6,
164
+ Gemini 4/3.x, Kimi K3, Qwen3.5/Coder, GLM-5.3, Grok 4.7, Llama 4,
165
+ Mistral Large 4, plus verified `:free` tiers). Context windows and
166
+ $/1M prices cross-checked against models.dev and the OpenRouter public
167
+ catalog.
168
+ - **Provider resilience — retries with backoff and honest error classes
169
+ (`src/providers/anthropic.js`, `openai-compat.js`).** Anthropic requests
170
+ retry 429/529/5xx up to 4 tries with jitter, honoring `Retry-After`;
171
+ openai-compat retries 429/502/503/529 exponentially (first retry is
172
+ immediate unless `Retry-After` says otherwise). Provider errors now carry
173
+ `.status`/`.retryAfterMs` so the session loop can act on them: rate limits
174
+ **wait and retry** (honoring `Retry-After`, capped 30s) before falling back
175
+ to model switching, `isQuotaError` (402/insufficient balance) still
176
+ auto-switches models, and `isContextOverflowError` **compacts and retries
177
+ once per turn**. Mid-stream SSE `error` events and fully-truncated streams
178
+ now throw instead of silently yielding an empty answer; Anthropic
179
+ `stop_reason` maps through to `finish_reason` (`max_tokens`→`length`,
180
+ `tool_use`→`tool_calls`) and tool results carry `is_error` on both the wire
181
+ and the history row (`isError`).
182
+ - **Session-loop resilience + context hygiene (`src/core/session.js`).**
183
+ `_normalizeHistory()` slices every provider call from the first user row
184
+ (no more leading orphan assistant/tool rows), compaction summaries are
185
+ stored as **user-role rows** — openai-compat drops `role:'system'` history
186
+ rows, so the summary used to silently vanish before the model ever saw it —
187
+ `estimateTokens()` counts the system prompt and tool schemas,
188
+ `contextEstimate()` backs a live `/context` window, and auto-compaction
189
+ triggers at the context threshold mid-turn (once per turn). Failed tool
190
+ outcomes are marked `isError` in history.
191
+ - **Two new tools — `todoread` and `tasks`** — read the current todo list,
192
+ and list/output/kill background tasks (`bg:*` handles, auto-gone after
193
+ 10min finished). Tool results are capped at **100k chars** (head + tail
194
+ with a `[…truncated N chars…]` marker) across text, errors, and MCP
195
+ payloads; `read` detects binaries and caps at 512KB / 2000 lines; `bash`
196
+ takes `timeoutMs` (default 60s) and is killed by Ctrl-C; `webfetch`
197
+ aborts with the session; `edit` gains a multi-edit `edits` array (each
198
+ entry enforces a unique match — the single `oldString` path keeps its
199
+ replace-first behavior).
200
+ - **MCP tools are permission-gated** — new `mcp` permission key (default
201
+ `ask`); patterns match the full tool name (`mcp__github__*`, exact names,
202
+ denies), and `todoread` maps to the `read` rule.
203
+ - **Project steering reads `AGENTS.md` (`src/core/memory.js`)** — global
204
+ LOOM.md plus cwd `LOOM.md`/`.loom/LOOM.md`/`AGENTS.md` and up to 3 parent
205
+ directories (per-file 12k, project-section 24k caps), with `CLAUDE.md` as
206
+ a fallback when no LOOM/AGENTS file exists.
207
+ - **Hooks: `userPromptSubmit` and `subagentStop` (`src/core/hooks.js`).**
208
+ `userPromptSubmit` runs before a prompt hits the model and can block the
209
+ prompt (`decision:"block"` or a non-zero exit with stderr) or inject
210
+ `<user-prompt-submit-hook>` context; `subagentStop` fires after a
211
+ subagent finishes; `postToolUse` now receives `tool_response` (capped 4k).
212
+ - **TUI/CLI:** a `/compact` slash command (also Ctrl+X c), `/context` shows
213
+ a live context-window estimate (tokens / window / %), background-task
214
+ completions surface as toasts, and per-turn autosave keeps saved sessions
215
+ current. `loom`'s `/compact` now uses the real `session.compact()`.
216
+ - **Easier connector installs — `loom add connector` + a visible "+ Add
217
+ connector…" row (`src/mcp/connector-presets.js`, `connectorAddLineCmd`).**
218
+ The connector preset catalog moved from the TUI into the core layer so the
219
+ shell and the TUI picker share one source. From the shell (or
220
+ `/connectors add`), a preset name plus `-e KEY=V` flags expands to the full
221
+ one-liner — `loom add connector supabase -e SUPABASE_ACCESS_TOKEN=xxx` —
222
+ and missing keys fail with the exact `-e` flag and where to get the token
223
+ instead of persisting a broken server. Leftover tokens append as trailing
224
+ args (`… supabase --project-ref abcdef`), and anything that is not a preset
225
+ name falls through to the generic `[-e KEY=V] <name> [--] <command>
226
+ [args...]` form. The `/connectors` and `/mcp` browsers now pin a "+ Add …"
227
+ row under the list (Enter opens the preset picker; `A` still works), and
228
+ the one-line add editor accepts a bare preset name.
27
229
  - **Docker sandbox with a guided `/sandbox` wizard (`src/core/sandbox.js`,
28
230
  `SandboxModal`).** `/sandbox` (or `/sandbox setup`) opens a three-step
29
231
  modal: (1) Docker check — if missing, shows the platform install link with
@@ -68,6 +270,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
68
270
  reads allow, interactive click/fill/press ask.
69
271
 
70
272
  ### Fixed
273
+ - **Unit tests no longer read the developer's real `~/.loom` config**
274
+ (`src/core/session.test.js`, `agents.test.js`). `session.test.js` never set
275
+ `LOOM_CONFIG_DIR`, and the config-merge test in `agents.test.js` DELETED
276
+ the variable instead of restoring its temp dir — so once the local config
277
+ picked a dynamic models.dev provider (or any provider the test process
278
+ hadn't merged), every `new Session()` failed with `Unknown provider` and
279
+ the whole `bun run test:unit` stage went red (36 failures). Both files now
280
+ run hermetically against a temp config dir (restored/cleaned in
281
+ `afterAll`).
71
282
  - **Agent loop no longer stops in the middle of work** (`src/core/session.js`
72
283
  continuation guards — opencode/Claude-Code parity):
73
284
  - **Truncation auto-continue.** `finish_reason=length` (output budget
@@ -86,6 +297,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
86
297
  todos are surfaced to the UI whenever the guard engaged.
87
298
 
88
299
  ### Changed
300
+ - **Sandbox commands are now routed through Docker when the agent decides it
301
+ needs the sandbox** — instead of quietly running on the host:
302
+ - `bash` gained an opt-in **`sandbox: true`** parameter; the same command
303
+ then runs via `docker exec loom-sandbox sh -lc "<command>"`
304
+ (`workdir /workspace`). If the sandbox isn't set up the call FAILS with
305
+ the `/sandbox` setup hint — it never silently falls back to the host.
306
+ - The system prompt now states the exact routing (build-mode tool list +
307
+ a per-turn **"Sandbox (Docker, ready)"** block, resolved lazily so
308
+ `/sandbox setup|on|off` applies from the next turn): sandbox-intended
309
+ commands go through the `sandbox` tool or `bash {sandbox:true}`, ordinary
310
+ commands stay on the host.
311
+ - The `sandbox` tool description spells out the `docker exec … sh -lc`
312
+ shape, and the tool now flows through the same permission gate as bash
313
+ (dangerous commands prompt; user-approved calls carry `_approved`).
89
314
  - **Sandbox is now opt-in per command, not a global router.** Enabling the
90
315
  sandbox no longer hijacks the bash tool — every host command keeps running
91
316
  on your machine (Windows included) exactly as before. Instead, a dedicated
@@ -99,6 +324,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
324
  also became fully non-blocking: docker pull streams per-layer progress
100
325
  into a live percentage bar, package installs stream their output, and the
101
326
  TUI stays responsive throughout (esc lets setup finish in the background).
327
+ - **Every docker status check is now spawn-based (non-blocking).** The last
328
+ synchronous `execFileSync` calls — `detectDocker()` (wizard open, setup
329
+ start, "docker is ready" resume), `sandboxStatus()` (session start,
330
+ `/sandbox status`, `/sandbox on`), and `/sandbox rm` — blocked the whole
331
+ event loop for up to 10–30 s while Docker Desktop answered, freezing the
332
+ TUI (no cursor, no renders) and swallowing live progress updates. New
333
+ async twins (`detectDockerAsync`, `sandboxStatusAsync`, `sandboxPending`)
334
+ stream results back via promises; the wizard shows a spinner during the
335
+ check instead of hanging; the session-start pending reminder is now a
336
+ config-only read (zero docker calls).
102
337
  - **Slash popup decluttered.** The autocomplete popup and command palette
103
338
  now show a short one-line description per command — argument syntax is no
104
339
  longer appended (it lives in `/help` and the README table). Duplicate
package/README.md CHANGED
@@ -21,11 +21,11 @@ An AI-powered coding agent for the terminal with multi-provider support and a fu
21
21
  - Full OpenTUI interface with slash commands, autocomplete, and ESC-to-interrupt
22
22
  - Build / Plan / Chat modes
23
23
  - OpenCode-style agents — primary agents (`build`/`plan`/`chat`) plus delegating subagents (`explore`, `scout`, `general`) invoked automatically via the `task` tool or manually via `@agent` mentions
24
- - LOOM.md project memory file
25
- - Multi-provider: Anthropic, OpenAI, NVIDIA, Google Gemini, OpenRouter, Local (Ollama)
26
- - Built-in tools: read, write, edit, bash, grep, glob, webfetch, todowrite
24
+ - LOOM.md / AGENTS.md project memory (cwd + parent directories)
25
+ - Multi-provider: Anthropic, OpenAI, NVIDIA, Google Gemini, OpenRouter, Local (Ollama) — with automatic retries (Retry-After aware), rate-limit waits, quota-driven model switching, and compact-and-retry on context overflow
26
+ - Built-in tools: read, write, edit (multi-edit), bash (timeouts + interrupt), grep, glob, webfetch, todowrite, todoread, tasks — results capped at 100k chars
27
27
  - Agentic tool loop (multiple tool calls per turn)
28
- - MCP (Model Context Protocol) server integration
28
+ - MCP (Model Context Protocol) server integration — MCP tools are permission-gated (`mcp` rule, default ask)
29
29
  - Drag-to-copy selections, paste support
30
30
  - Session memory, /undo, /redo, /compact, /reset, /fork
31
31
  - One-shot print mode: `loom -p "query"`
@@ -33,6 +33,7 @@ An AI-powered coding agent for the terminal with multi-provider support and a fu
33
33
  - OpenCode-style agent harness: main orchestrator agent delegates to specialist subagents (`research`, `coder`, `reviewer`, `tester`, `web`, `explore`, `scout`) via the `task` tool, and a drive-to-completion loop keeps working while todos are still open
34
34
  - Headless browser tool (`browser`) — navigate, click, fill, read the DOM, and screenshot real pages from the `web` agent
35
35
  - Usage & billing tracking per session + lifetime
36
+ - Hooks: `userPromptSubmit` (block or inject context before a prompt runs) and `subagentStop` in `.loom/hooks.json`
36
37
 
37
38
  ## Prerequisites
38
39
 
@@ -128,7 +129,7 @@ Zed (add to `~/.config/zed/settings.json`, then Command Palette → `agent: new
128
129
  Full transport spec, JetBrains/Neovim configs, the protocol walkthrough, and a
129
130
  self-test client (`node scripts/acp-smoke.js`) live in **[docs/acp.md](docs/acp.md)**.
130
131
 
131
- ## Slash Commands (36 total)
132
+ ## Slash Commands (37 total)
132
133
 
133
134
  | Command | Args | Description |
134
135
  |---------|------|-------------|
@@ -154,10 +155,11 @@ self-test client (`node scripts/acp-smoke.js`) live in **[docs/acp.md](docs/acp.
154
155
  | `/graph` | — | Memory graph |
155
156
  | `/skills` | `install\|remove` | Manage skills |
156
157
  | `/mcp` | `add\|remove\|toggle` | Manage MCP servers |
157
- | `/connectors` | `add\|remove\|toggle` | Manage connectors |
158
+ | `/connectors` | `add\|remove\|toggle` | Manage connectors — presets (supabase, railway, vercel, …) or one-liners; also `loom add connector` |
158
159
  | `/permissions` | `reset\|auto` | Permission rules |
159
160
  | `/subagents` | — | Subagent runs |
160
- | `/context` | — | Context window usage |
161
+ | `/context` | — | Context window usage (live estimate) |
162
+ | `/compact` | — | Summarize older messages to free context |
161
163
  | `/think` | `off\|low\|medium\|high` | Thinking budget |
162
164
  | `/approve` | — | Approve plan → Build |
163
165
  | `/tasks` | — | Background tasks |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loom-agent",
3
- "version": "1.3.3",
3
+ "version": "1.6.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/toshalkumbhar8979-design/loomcode.git"
@@ -24,7 +24,7 @@
24
24
  "dev": "node --watch src/index.js",
25
25
  "setup": "node src/setup.js",
26
26
  "test": "bun run src/tui/test-interactive.tsx && bun run test:unit && bun run lint:core",
27
- "test:unit": "bun test src/core/agents.test.js src/core/hooks.test.js src/core/custom-commands.test.js src/core/background-tasks.test.js src/core/memory.test.js src/core/subagent-log.test.js src/core/session.test.js src/core/sandbox.test.js src/tools/index.test.js src/tools/security.test.js src/providers/providers.test.js src/providers/caching.test.js src/providers/caching-thinking.test.js src/providers/registry.test.js src/mcp/mcp-client.test.js src/mcp/mcp-manager.test.js src/core/session-store.test.js src/skills/skills-manager.test.js src/core/usage.test.js src/core/permissions.test.js src/core/format.test.js src/acp/acp-server.test.js src/tui/keybinds.test.ts",
27
+ "test:unit": "bun test src/core/agents.test.js src/core/hooks.test.js src/core/custom-commands.test.js src/core/background-tasks.test.js src/core/memory.test.js src/core/subagent-log.test.js src/core/session.test.js src/core/sandbox.test.js src/tools/index.test.js src/tools/security.test.js src/providers/providers.test.js src/providers/caching.test.js src/providers/caching-thinking.test.js src/providers/registry.test.js src/mcp/mcp-client.test.js src/mcp/mcp-manager.test.js src/core/session-store.test.js src/skills/skills-manager.test.js src/core/usage.test.js src/core/permissions.test.js src/core/format.test.js src/acp/acp-server.test.js src/tui/keybinds.test.ts src/core/transcript-undo.test.js src/core/update.test.js",
28
28
  "lint:core": "node node_modules/typescript/bin/tsc -p tsconfig.core.json",
29
29
  "smoke:acp": "node scripts/acp-smoke.js",
30
30
  "tui": "bun bin/loom-tui.js",
@@ -20,12 +20,17 @@ function configFile() {
20
20
  const DEFAULTS = {
21
21
  provider: 'anthropic',
22
22
  model: {
23
- anthropic: 'claude-sonnet-4-20250514',
24
- openai: 'gpt-5-fast',
25
- nvidia: 'meta/llama-3.1-8b-instruct',
23
+ // claude-sonnet-4-20250514 was retired 2026-06-15 (404); Anthropic's
24
+ // recommended replacement is claude-sonnet-5-5.
25
+ anthropic: 'claude-sonnet-5-5',
26
+ // Defaults must be IDs that exist on the LIVE provider APIs (verified
27
+ // 2026-10-17): gpt-5-fast and meta/llama-3.1-8b-instruct returned 404,
28
+ // anthropic/claude-sonnet-4 and moonshotai/kimi-k3-free are gone too.
29
+ openai: 'gpt-5',
30
+ nvidia: 'nvidia/nemotron-3-super-120b-a12b',
26
31
  google: 'gemini-2.5-flash',
27
- openrouter: 'anthropic/claude-sonnet-4',
28
- tokenrouter: 'moonshotai/kimi-k3-free',
32
+ openrouter: 'anthropic/claude-sonnet-5.5',
33
+ tokenrouter: 'z-ai/glm-5.3-free',
29
34
  local: 'llama3.2',
30
35
  },
31
36
  maxTokens: 16384,
@@ -62,7 +62,7 @@ const BUILTIN_AGENTS = {
62
62
  id: 'build', name: 'Build', mode: 'primary',
63
63
  description: 'Main orchestrator agent: full development work with all tools, drives tasks to completion.',
64
64
  tools: ['*'],
65
- prompt: 'You are the MAIN ORCHESTRATOR agent. For non-trivial, multi-step work: (1) capture the plan as a todowrite list up front, (2) keep working until every item is completed or explicitly cancelled — never stop mid-task with a summary while todos remain, (3) delegate independent/parallelizable packages via the task tool (research for deep investigation, coder for isolated implementation, reviewer for review, tester for tests, web for browser work, explore for fast code search), (4) verify your work (run tests/typecheck/lint the project actually uses) before declaring done, (5) finish with a terse summary of what changed.',
65
+ prompt: "You are the MAIN ORCHESTRATOR agent. You DELEGATE heavy lifting to subagents via the task tool and synthesize their reports — you are rarely the one reading raw files. For any task that is a SEARCH, ANALYSIS, or broad OPTIMIZATION (\"find what causes X\", \"where is Y\", \"how does Z work\", \"optimize W\", \"audit/investigate/locate\"), FAN OUT FIRST: split the scope into independent questions and dispatch them as parallel task calls in a single message — one explore agent per disjoint area of the codebase (parallel calls run concurrently). Only read/grep a file YOURSELF when you need one specific line a subagent's report left ambiguous. Then: (1) for non-trivial multi-step work capture the plan as a todowrite list up front, (2) keep working until every item is completed or explicitly cancelled — never stop mid-task with a summary while todos remain, (3) dispatch implementation/verification packages too: coder for isolated implementation, reviewer for review, tester for tests, research for deep multi-source investigation, web for browser work, (4) verify your work (run tests/typecheck/lint the project actually uses) before declaring done, (5) finish with a terse summary of what changed and which subagent reports you folded in.",
66
66
  },
67
67
  plan: {
68
68
  id: 'plan', name: 'Plan', mode: 'primary',
@@ -335,6 +335,19 @@ async function runSubagent(opts) {
335
335
  const durationMs = Date.now() - t0;
336
336
  const interrupted = !!resp.interrupted;
337
337
 
338
+ // subagentStop hook — fires when a subagent turn ends (success, error,
339
+ // interrupt). Fire-and-forget: a slow/broken hook must never wedge the
340
+ // parent turn.
341
+ try {
342
+ const hooks = require('./hooks');
343
+ Promise.resolve(hooks.runHook('subagentStop', {
344
+ agent: agent.id,
345
+ runId,
346
+ reason: resp.type || 'end',
347
+ interrupted,
348
+ })).catch(() => {});
349
+ } catch {}
350
+
338
351
  if (parent) {
339
352
  parent.tokensIn += child.tokensIn;
340
353
  parent.tokensOut += child.tokensOut;
package/src/core/cli.js CHANGED
@@ -304,8 +304,14 @@ class LoomCLI {
304
304
  }
305
305
 
306
306
  async cmdCompact() {
307
- this.session.messages = this.session.messages.slice(-10);
308
- console.log('Conversation compacted - keeping last 10 messages.');
307
+ // Real compaction: summarize the older messages with the model (falls
308
+ // back to head-truncation when no provider/key is available).
309
+ const res = await this.session.compact();
310
+ if (res.compacted) {
311
+ console.log(`Compacted (${res.method}): removed ${res.removed} messages, ~${res.tokensAfter} tokens remain.`);
312
+ } else {
313
+ console.log('Nothing to compact — the conversation is still short.');
314
+ }
309
315
  }
310
316
 
311
317
  cmdUndo() {
@@ -540,6 +546,7 @@ if (args.includes('--help') || args.includes('-h')) {
540
546
  console.log(' loom --auto Auto-approve permission prompts');
541
547
  console.log(' loom acp ACP server (JSON-RPC over stdio, for editor plugins)');
542
548
  console.log(' loom add mcp <spec> Add an MCP server (same syntax as /mcp add)');
549
+ console.log(' loom add connector <spec> Add a connector (preset like supabase, or one-liner)');
543
550
  console.log(' loom -s <session-id> Resume a saved session');
544
551
  console.log(' loom --version Show version');
545
552
  console.log(' loom --help Show this help');
@@ -576,6 +583,27 @@ if (args.includes('--help') || args.includes('-h')) {
576
583
  process.exit(ok ? 0 : 1);
577
584
  }
578
585
 
586
+ // loom add connector <spec> — same engine as /connectors add. A preset name
587
+ // (supabase, nextjs, railway, vercel, netlify, cloudflare) expands with
588
+ // -e KEY=V flags; anything else is the generic one-liner form. Exits 0 on
589
+ // success.
590
+ if (args[0] === 'add' && (args[1] === 'connector' || args[1] === 'connectors')) {
591
+ const spec = args.slice(2);
592
+ if (!spec.length) {
593
+ const { CONNECTOR_PRESETS } = require('../mcp/connector-presets');
594
+ console.log('Usage: loom add connector <preset> [-e KEY=V]...');
595
+ console.log(' or: loom add connector [-e KEY=V] <name> [--] <command> [args...]');
596
+ console.log('Presets: ' + CONNECTOR_PRESETS.map(p => p.id).join(', '));
597
+ console.log('Example: loom add connector supabase -e SUPABASE_ACCESS_TOKEN=xxx');
598
+ process.exit(1);
599
+ }
600
+ const { connectorAddLineCmd } = require('./plugin-cmd');
601
+ const msg = connectorAddLineCmd(spec.join(' '));
602
+ const ok = typeof msg === 'string' && msg.startsWith('Added');
603
+ console.log(ok ? msg : 'Error: ' + msg);
604
+ process.exit(ok ? 0 : 1);
605
+ }
606
+
579
607
  if (sessionId && sIndex !== -1) {
580
608
  const data = loadSession(sessionId);
581
609
  if (data && data.messages && data.messages.length) {
package/src/core/hooks.js CHANGED
@@ -2,27 +2,33 @@
2
2
  // agent loop (Claude Code-style). Config lives in config.json:
3
3
  //
4
4
  // "hooks": {
5
+ // "userPromptSubmit": "node .loom/hooks/ctx.js", // may BLOCK the prompt or inject context
5
6
  // "preToolUse": "node .loom/hooks/guard.js", // may BLOCK the tool call
6
- // "postToolUse": "node .loom/hooks/audit.js", // informational
7
- // "stop": "node .loom/hooks/notify.js" // fires when a turn ends
7
+ // "postToolUse": "node .loom/hooks/audit.js", // informational (gets tool_response)
8
+ // "stop": "node .loom/hooks/notify.js", // fires when a turn ends
9
+ // "subagentStop": "node .loom/hooks/done.js" // fires when a subagent finishes
8
10
  // }
9
11
  //
10
- // Contract: the command receives JSON on stdin ({ hook, tool, input } for
11
- // pre/post, { hook, reason } for stop) plus LOOM_HOOK / LOOM_TOOL /
12
+ // Contract: the command receives JSON on stdin ({ hook, tool, input,
13
+ // tool_response } for pre/post, { hook, prompt } for userPromptSubmit,
14
+ // { hook, reason } for stop/subagentStop) plus LOOM_HOOK / LOOM_TOOL /
12
15
  // LOOM_TOOL_INPUT env vars (Windows one-liner friendly). Exit code non-zero,
13
- // or stdout containing {"decision":"deny","reason":"..."}, blocks a
14
- // preToolUse hook's tool call. A 10s timeout counts as a failure-to-run
15
- // (allowed through) rather than a deny — hooks must not wedge the loop.
16
+ // or stdout containing {"decision":"deny"|"block","reason":"..."}, blocks a
17
+ // preToolUse tool call or a userPromptSubmit prompt. userPromptSubmit stdout
18
+ // may also carry {"hookSpecificOutput":{"additionalContext":"..."}} (or a
19
+ // top-level additionalContext) which is appended to the user's message.
20
+ // A 10s timeout counts as a failure-to-run (allowed through) rather than a
21
+ // deny — hooks must not wedge the loop.
16
22
  const { spawn } = require('child_process');
17
23
  const { loadConfig } = require('../config/settings');
18
24
 
19
- /** @typedef {'preToolUse'|'postToolUse'|'stop'} HookName */
25
+ /** @typedef {'preToolUse'|'postToolUse'|'stop'|'userPromptSubmit'|'subagentStop'} HookName */
20
26
 
21
27
  /**
22
28
  * Run one configured hook.
23
29
  * @param {HookName} name
24
- * @param {{ tool?: string, input?: object, reason?: string }} payload
25
- * @returns {Promise<{ blocked: boolean, reason?: string, ran: boolean }>}
30
+ * @param {{ tool?: string, input?: object, reason?: string, prompt?: string, tool_response?: string, agent?: string, runId?: string, interrupted?: boolean }} payload
31
+ * @returns {Promise<{ blocked: boolean, reason?: string, ran: boolean, additionalContext?: string }>}
26
32
  */
27
33
  async function runHook(name, payload) {
28
34
  let cmd = '';
@@ -61,20 +67,28 @@ async function runHook(name, payload) {
61
67
  child.on('error', () => { clearTimeout(timer); done({ blocked: false, ran: false }); });
62
68
  child.on('close', (code) => {
63
69
  clearTimeout(timer);
64
- // Structured deny wins over exit codes; plain non-zero also blocks preToolUse.
70
+ // Structured deny wins over exit codes; plain non-zero also blocks
71
+ // preToolUse and userPromptSubmit.
65
72
  let denyReason = null;
73
+ let additionalContext = null;
66
74
  const m = out.match(/\{[\s\S]*\}/);
67
75
  if (m) {
68
76
  try {
69
77
  const j = JSON.parse(m[0]);
70
- if (j && j.decision === 'deny') denyReason = String(j.reason || 'blocked by hook');
78
+ if (j && (j.decision === 'deny' || j.decision === 'block')) {
79
+ denyReason = String(j.reason || 'blocked by hook');
80
+ }
81
+ const ctxText = j && (j.additionalContext
82
+ || (j.hookSpecificOutput && j.hookSpecificOutput.additionalContext));
83
+ if (ctxText) additionalContext = String(ctxText);
71
84
  } catch {}
72
85
  }
73
- if (name === 'preToolUse' && (denyReason || code !== 0)) {
86
+ const blockable = name === 'preToolUse' || name === 'userPromptSubmit';
87
+ if (blockable && (denyReason || code !== 0)) {
74
88
  done({ blocked: true, reason: denyReason || ('hook exited ' + code), ran: true });
75
89
  return;
76
90
  }
77
- done({ blocked: false, ran: true });
91
+ done({ blocked: false, ran: true, additionalContext: additionalContext || undefined });
78
92
  });
79
93
  });
80
94
  }
@@ -1,6 +1,9 @@
1
- // Memory — layered LOOM.md files injected into every session's system prompt.
2
- // Layers: global (~/.loom/LOOM.md) then project (<cwd>/LOOM.md). Any file may
3
- // pull in others with `@relative/path.md` lines (depth-capped, cycle-safe).
1
+ // Memory — layered steering files injected into every session's system prompt.
2
+ // Layers: global (~/.loom/LOOM.md), then project steering: LOOM.md and
3
+ // AGENTS.md in <cwd> plus up to 3 parent directories (monorepo repos carry
4
+ // steering at the root). CLAUDE.md loads only when the cwd has neither
5
+ // LOOM.md nor AGENTS.md (back-compat shim). Any file may pull in others with
6
+ // `@relative/path.md` lines (depth-capped, cycle-safe).
4
7
  const fs = require('fs');
5
8
  const path = require('path');
6
9
  const os = require('os');
@@ -8,6 +11,10 @@ const os = require('os');
8
11
  const MAX_IMPORT_DEPTH = 3;
9
12
  // Cap each layer so a runaway memory file can't eat the context window.
10
13
  const MAX_CHARS_PER_LAYER = 12000;
14
+ // Hard cap for the whole project section (4 dirs × 2 filenames worst case).
15
+ const MAX_CHARS_PROJECT = 24000;
16
+ // How many directories up from cwd carry steering files.
17
+ const MAX_PARENT_DEPTH = 3;
11
18
 
12
19
  function globalMemoryPath() {
13
20
  const base = process.env.LOOM_CONFIG_DIR || path.join(os.homedir(), '.loom');
@@ -40,6 +47,35 @@ function loadFile(file, depth, seen) {
40
47
  return out.join('\n').trim();
41
48
  }
42
49
 
50
+ /**
51
+ * Collect project steering files: cwd + up to MAX_PARENT_DEPTH parents,
52
+ * LOOM.md then AGENTS.md per directory, nearest first. Returns
53
+ * [{ file, body }] with imports expanded.
54
+ * @returns {Array<{file: string, body: string}>}
55
+ */
56
+ function collectProjectSteering() {
57
+ const chunks = [];
58
+ const seen = new Set();
59
+ let dir = process.cwd();
60
+ for (let depth = 0; depth <= MAX_PARENT_DEPTH; depth++) {
61
+ const names = depth === 0
62
+ ? ['LOOM.md', '.loom/LOOM.md', 'AGENTS.md']
63
+ : ['LOOM.md', 'AGENTS.md'];
64
+ for (const name of names) {
65
+ const p = path.join(dir, name);
66
+ const abs = path.resolve(p);
67
+ if (seen.has(abs)) continue;
68
+ seen.add(abs);
69
+ const body = loadFile(p, 0, new Set());
70
+ if (body) chunks.push({ file: abs, body });
71
+ }
72
+ const parent = path.dirname(dir);
73
+ if (parent === dir) break;
74
+ dir = parent;
75
+ }
76
+ return chunks;
77
+ }
78
+
43
79
  /**
44
80
  * Build the full memory block: "## Global memory" + "## Project memory"
45
81
  * sections with imports expanded. Empty string when no memory exists.
@@ -49,9 +85,21 @@ function loadMemory() {
49
85
  const parts = [];
50
86
  const g = loadFile(globalMemoryPath(), 0, new Set());
51
87
  if (g) parts.push('## Global memory\n\n' + g.slice(0, MAX_CHARS_PER_LAYER));
52
- let p = loadFile(path.join(process.cwd(), 'LOOM.md'), 0, new Set());
53
- if (!p) p = loadFile(path.join(process.cwd(), '.loom', 'LOOM.md'), 0, new Set());
54
- if (p) parts.push('## Project memory\n\n' + p.slice(0, MAX_CHARS_PER_LAYER));
88
+ let chunks = collectProjectSteering();
89
+ // CLAUDE.md parity: only when no LOOM.md/AGENTS.md steering exists in cwd
90
+ // or its parents — a repo that already steers via LOOM/AGENTS wins.
91
+ if (!chunks.length) {
92
+ const p = path.join(process.cwd(), '.claude', 'CLAUDE.md');
93
+ const body = loadFile(p, 0, new Set());
94
+ if (body) chunks.push({ file: p, body });
95
+ }
96
+ if (chunks.length) {
97
+ const content = chunks
98
+ .map((c) => '### ' + c.file + '\n\n' + c.body.slice(0, MAX_CHARS_PER_LAYER))
99
+ .join('\n\n')
100
+ .slice(0, MAX_CHARS_PROJECT);
101
+ parts.push('## Project memory\n\n' + content);
102
+ }
55
103
  return parts.join('\n\n');
56
104
  }
57
105