@turmind/halo 0.1.9 → 0.2.1-bc7cf2b

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 (66) hide show
  1. package/README.md +1 -5
  2. package/admin-out/404.html +1 -1
  3. package/admin-out/_next/static/chunks/212-170855bed19f67f3.js +1 -0
  4. package/admin-out/_next/static/chunks/{503-7c6d9935df6e0cea.js → 307-d7a99f9a4a436ec3.js} +1 -1
  5. package/admin-out/_next/static/chunks/525.2c10a4fd329d0d24.js +1 -0
  6. package/admin-out/_next/static/chunks/app/layout-c2fa8035998e48d5.js +1 -0
  7. package/admin-out/_next/static/chunks/app/page-31317ef1fb0ba31f.js +1 -0
  8. package/admin-out/_next/static/chunks/{webpack-8bd7ef88a6527feb.js → webpack-47520cdc55be63e7.js} +1 -1
  9. package/admin-out/_next/static/css/66df341f99220f5c.css +3 -0
  10. package/admin-out/index.html +1 -1
  11. package/admin-out/index.txt +21 -20
  12. package/bundled-docs/design/agent.md +1 -1
  13. package/bundled-docs/design/evolution.md +11 -2
  14. package/bundled-docs/design/halo-city.md +31 -0
  15. package/bundled-docs/design/prompt-system.md +3 -3
  16. package/bundled-docs/design/session.md +26 -1
  17. package/bundled-docs/design/storage.md +13 -0
  18. package/bundled-docs/dev/acp-adapter.md +31 -4
  19. package/bundled-docs/dev/api.md +6 -4
  20. package/bundled-docs/dev/desktop-packaging.md +98 -8
  21. package/bundled-docs/dev/env.md +4 -2
  22. package/bundled-docs/dev/previews.md +3 -0
  23. package/bundled-docs/dev/tools.md +3 -1
  24. package/bundled-docs/guide/cli.md +40 -16
  25. package/bundled-docs/guide/getting-started.md +3 -2
  26. package/bundled-docs/requirements/editor.md +1 -1
  27. package/bundled-docs/requirements/explorer.md +11 -0
  28. package/dist/index.js +14102 -13249
  29. package/package.json +1 -2
  30. package/templates/INSTRUCTIONS.md +0 -4
  31. package/templates/agents/__apply_agent__/AGENT.md +41 -19
  32. package/templates/agents/__evo_agent__/AGENT.md +73 -45
  33. package/templates/agents/__evo_agent__/agent.yaml +9 -6
  34. package/templates/agents/__score__/AGENT.md +65 -33
  35. package/templates/agents/deep-executor/AGENT.md +23 -6
  36. package/templates/agents/deep-executor/agent.yaml +4 -1
  37. package/templates/agents/default/AGENT.md +32 -8
  38. package/templates/agents/executor/AGENT.md +19 -1
  39. package/templates/agents/executor/agent.yaml +6 -3
  40. package/templates/models/aws-bedrock-claude-invoke.yaml +53 -0
  41. package/templates/prompts/all/WORKSPACE_CONVENTIONS.md +5 -0
  42. package/templates/prompts/bootstrap/BOOTSTRAP.md +4 -0
  43. package/templates/prompts/root/WORKSPACE_MEMORY.md +2 -0
  44. package/templates/skills/acp/SKILL.md +8 -1
  45. package/templates/skills/acp/templates/SKILL.md.tmpl +2 -1
  46. package/templates/skills/acp/templates/ask.py +2 -1
  47. package/templates/skills/acp/templates/config.yaml.tmpl +5 -0
  48. package/admin-out/_next/static/chunks/298-d6b224b569da4de0.js +0 -1
  49. package/admin-out/_next/static/chunks/661.a4fe93f3f2a5e871.js +0 -1
  50. package/admin-out/_next/static/chunks/app/layout-58f773336e6fce61.js +0 -1
  51. package/admin-out/_next/static/chunks/app/page-cc8043335282d745.js +0 -1
  52. package/admin-out/_next/static/css/8fce4e1a54dac12a.css +0 -3
  53. package/bundled-docs/plans/cron-tasks.md +0 -362
  54. package/bundled-docs/plans/design-suggestions.md +0 -203
  55. package/bundled-docs/plans/issue-session-deep-nesting.md +0 -208
  56. package/bundled-docs/plans/launch-plan.md +0 -111
  57. package/bundled-docs/plans/md-and-agent-refactor.md +0 -197
  58. package/bundled-docs/plans/remote-mac-worker.md +0 -196
  59. package/bundled-docs/plans/roadmap.md +0 -40
  60. package/bundled-docs/plans/self-evolution.md +0 -1379
  61. package/bundled-docs/plans/todo.md +0 -32
  62. package/bundled-docs/plans/weixin-channel.md +0 -255
  63. package/bundled-docs/test/docs-seeding.md +0 -101
  64. package/bundled-docs/test/session.md +0 -374
  65. /package/admin-out/_next/static/{IBP_mwBDf-cPwZSZZGpi8 → iSrc6429pyTB0lK-EE5oH}/_buildManifest.js +0 -0
  66. /package/admin-out/_next/static/{IBP_mwBDf-cPwZSZZGpi8 → iSrc6429pyTB0lK-EE5oH}/_ssgManifest.js +0 -0
@@ -43,7 +43,7 @@ Claude Code on a developer's laptop wants to talk to a halo agent running in an
43
43
 
44
44
  The most common use of this adapter isn't a third-party ACP client — it's *another halo agent* calling out over ACP. Halo ships a builtin skill `acp` (slash command `/acp`, full access). Its bundled `templates/ask.py` is a unified ACP client; `--kind` picks the peer:
45
45
 
46
- - `halo` (default) — spawns `halo acp --host --port --token --workspace` (this adapter) to reach a **remote halo server**
46
+ - `halo` (default) — spawns `halo acp --host --port --scheme --token --workspace` (this adapter) to reach a **remote halo server** (`--scheme` optional, default `http`; pass `https` for a TLS-fronted server)
47
47
  - `claude` — spawns `claude-agent-acp` (npm `@agentclientprotocol/claude-agent-acp`): local Claude Code, zero config, just `--cwd`
48
48
  - `kiro` — spawns `kiro-cli acp --trust-all-tools`: local Kiro, zero config `--cwd`, optional `--agent-id`
49
49
 
@@ -58,7 +58,7 @@ The question text reaches the peer **verbatim — including the peer's own slash
58
58
 
59
59
  ### `ask-*` bindings — the multi-remote-halo path
60
60
 
61
- Remote halo servers are **not** a direct verb: each remote needs its own host/token/workspace, so `/acp add` walks the user through (label, host, port, workspace, token), then **stamps out a new skill** named `ask-<label>` containing:
61
+ Remote halo servers are **not** a direct verb: each remote needs its own host/token/workspace, so `/acp add` walks the user through (label, host, port, scheme, workspace, token), then **stamps out a new skill** named `ask-<label>` containing:
62
62
 
63
63
  - `SKILL.md` — slash command `/ask-<label>`, instructions tailored to this remote
64
64
  - `config.yaml` — declares the binding's params so admin Settings shows a form
@@ -77,12 +77,39 @@ To remove a binding: `/acp remove` (deletes the skill directory and points out t
77
77
  |------------------|----------|------------------------------------------------------------------------------------------|
78
78
  | `--host` | yes | Halo server hostname / IP (e.g. `localhost`, `ec2-1-2-3-4.compute…`). |
79
79
  | `--port` | yes | Halo server port (e.g. `9527`). |
80
+ | `--scheme` | no | URL scheme — `http` or `https` (default `http`). Use `https` when the server sits behind a TLS reverse proxy. Invalid values are rejected at the adapter boundary. |
80
81
  | `--token` | yes | Web-channel token from admin UI. `full` access required for multi-workspace use. |
81
82
  | `--workspace` | yes | Absolute server-side path for the workspace this adapter drives. |
82
83
  | `--agent-id` | no | Halo agent profile to use when ACP `session/new` creates a new halo session. Default: `default`. |
84
+ | `--header` | no | Extra HTTP header on every upstream request, `"Name: value"` (like `curl -H`). Repeatable. For auth that sits **in front of** the halo server — see "Upstream auth" below. |
83
85
 
84
86
  One adapter process binds to one workspace. To drive multiple workspaces concurrently from the same token, run multiple adapter processes — see "Multi-workspace" below.
85
87
 
88
+ ## Upstream auth (`--header`)
89
+
90
+ `--token` authenticates the adapter to *halo itself* (it becomes the `x-token` header on every web-channel call). It does **not** cover a proxy sitting in front of the server — an SSO/session-cookie gateway, Cloudflare Access, ALB OIDC, nginx basic-auth. Those reject the request before halo ever sees the token.
91
+
92
+ `--header` is the generic escape hatch: it forwards arbitrary headers on every request the adapter makes (`/api/web/chat`, `/history`, `/stop`), exactly like `curl -H`. The adapter deliberately knows nothing about any specific gateway — you supply whatever that layer wants:
93
+
94
+ ```sh
95
+ # session-cookie gateway
96
+ halo acp --host h --port 9527 --scheme https --token <t> --workspace /ws \
97
+ --header "Cookie: <the gateway's session cookie, verbatim>"
98
+
99
+ # Cloudflare Access (two headers — --header is repeatable)
100
+ halo acp … --header "CF-Access-Client-Id: <id>" --header "CF-Access-Client-Secret: <secret>"
101
+
102
+ # HTTP basic auth
103
+ halo acp … --header "Authorization: Basic <base64 user:pass>"
104
+ ```
105
+
106
+ Notes:
107
+
108
+ - **Repeatable** — pass `--header` as many times as the gateway needs.
109
+ - **Colon-safe** — the value is split on the *first* `:` only, so header values that themselves contain colons (`Cookie: a=b:c`) survive intact.
110
+ - **`x-token` always wins** — `--header` cannot override the adapter's own `x-token` / `content-type`; it only adds headers for the layer in front of halo.
111
+ - The generated `ask-<label>` bindings (halo→halo, via `ask.py`) do **not** yet expose `--header`; upstream auth through a binding isn't wired. `halo acp` direct is the path when a gateway is in play.
112
+
86
113
  ## ACP method coverage
87
114
 
88
115
  | Method | Implemented? | Notes |
@@ -299,7 +326,7 @@ halo cli -a default -n -w /home/ubuntu/halo-test \
299
326
  '/acp add 参数:label=foo,host=localhost,port=9527,workspace=/home/ubuntu/sa-agent,token=<token>,scope=workspace。不要问后续问题,全自动创建。'
300
327
  ```
301
328
 
302
- Expect: agent creates `.halo/skills/ask-foo/{SKILL.md,config.yaml,ask.py}`, writes `ask-foo` block to `<workspace>/.halo/settings.yaml` with **all 5 user values** (host/port/workspace/label/token), wires the binding into the current agent's skills list. Reply confirms the four paths.
329
+ Expect: agent creates `.halo/skills/ask-foo/{SKILL.md,config.yaml,ask.py}`, writes `ask-foo` block to `<workspace>/.halo/settings.yaml` with **all 5 user values** (host/port/workspace/label/token) plus `scheme: http` (defaulted — the prompt omits it), wires the binding into the current agent's skills list. Reply confirms the four paths.
303
330
 
304
331
  **4.2 invoke the freshly-generated binding**
305
332
 
@@ -321,7 +348,7 @@ rm -rf /home/ubuntu/halo-test/.halo/skills/ask-foo
321
348
 
322
349
  Open admin → Settings → Skills → **Ask SA Agent** (or whichever binding):
323
350
 
324
- **5.1** All 6 fields render: `host`, `port`, `workspace`, `label`, `agent_id`, `token` (with mask icon).
351
+ **5.1** All 7 fields render: `host`, `port`, `scheme`, `workspace`, `label`, `agent_id`, `token` (with mask icon).
325
352
 
326
353
  **5.2** Each field's source label says **workspace** (not "继承自 global"), because values live in `<halo-test>/.halo/settings.yaml`.
327
354
 
@@ -235,10 +235,12 @@ runtime so the frontend can render rooms (workspaces) + characters (sessions).
235
235
 
236
236
  | Method | Path | Purpose |
237
237
  |---|---|---|
238
- | GET | `/api/show/state` | Full-access token → every known workspace; otherwise the account's own. Returns `{ serverTime, uptime, accessLevel, skills[], workspaces[] }` |
239
- | GET | `/api/show/session?ws=&id=` | Inspector-panel detail for a single session. Trimmed message log (last 40, content/tool I/O capped) plus `contextTokens` / `outputTokens` / `maxContextTokens` / `isRunning`. Non-full tokens may only read their own workspace. |
238
+ | GET | `/api/show/state` | `full` or `observer` token → every known workspace; otherwise the account's own. Returns `{ serverTime, uptime, accessLevel, skills[], workspaces[] }` |
239
+ | GET | `/api/show/session?ws=&id=` | Inspector-panel detail for a single session. Trimmed message log (last 40, content/tool I/O capped) plus `contextTokens` / `outputTokens` / `maxContextTokens` / `isRunning`. Non-`full`/`observer` tokens may only read their own workspace. |
240
240
 
241
- Each `workspace` = `{ path, key, label, counts{running,idle,stopped}, totalSessions, skills[], sessions[] }`; each `session` = `{ id, parentId, depth, agentName, description, status, lastTool, activeSkill, contextTokens, outputTokens, updatedAt }`. `lastTool` / `activeSkill` come from the live in-memory UI log (empty when the session isn't loaded). Sessions per room are capped (`totalSessions` reports the true total). Frontend: `halo-city/` at repo root.
241
+ Each `workspace` = `{ path, key, label, counts{running,idle,stopped}, totalSessions, skills[], sessions[] }`; each `session` = `{ id, parentId, depth, agentName, description, status, lastTool, activeSkill, contextTokens, outputTokens, messageCount, updatedAt }`. `lastTool` / `activeSkill` come from the live in-memory UI log (empty when the session isn't loaded). Sessions per room are capped (`totalSessions` reports the true total). Frontend: `halo-city/` at repo root.
242
+
243
+ **Read-only by construction — degraded snapshots.** Both endpoints resolve a workspace's runtime via `registry.peek()` — in-memory lookup only, **never** `getOrCreate`: constructing a SessionManager has write side effects (boot orphan-reconciliation batch-stops live sub-session rows; `.halo/` scaffolding), so a pure visualization poll must not trigger it. When no live SessionManager exists in this process, the handler opens that workspace's `.halo/halo.db` **read-only** (cached connection; missing db/tables silently degrade to an empty room) and projects rows / the persisted session file into the same wire shape. Sessions not in memory therefore return a **persisted snapshot**: `status` / token counts / `messageCount` may lag behind the last persistence point, and the live-only signals (`lastTool` / `activeSkill`) are empty by definition. The degraded `/api/show/session` path reports `maxContextTokens: 0` (the real cap needs a live agent config; the city inspector hides the meter when the cap is unknown).
242
244
 
243
245
  ## Agent Configs
244
246
 
@@ -312,7 +314,7 @@ File: `packages/server/src/routes/evolution.ts`. Surfaces the global `evolution_
312
314
  | Method | Path | Purpose |
313
315
  |---|---|---|
314
316
  | GET | `/api/evolution/runs` | List all evolution runs across workspaces. Carries the latest `apply_id`/`apply_status` per run. (Score isn't surfaced in the list — it's read from `score.json` on the detail fetch only.) |
315
- | GET | `/api/evolution/runs/:id` | Detail: db row + `patch.md` + `score.json` + `system-suggestions.md` (optional, evo writes when it has platform-level feedback) + `.skip.md` (when `status='skipped'`) + a snapshot summary (first user message + first assistant reply + message count). |
317
+ | GET | `/api/evolution/runs/:id` | Detail: db row + `patch.md` + `score.json` + `.skip.md` (when `status='skipped'`) + wrapper/sub-cli logs + a snapshot summary (first user message + first assistant reply + message count). Row carries `failureReason` (and `applyFailureReason` from the latest apply) — apply-side values include `phase A': apply cli exited <code>`, `phase A': apply agent aborted: <first line of ABORT.md>`, and `phase A': apply agent didn't produce apply.log`. |
316
318
  | POST | `/api/evolution/runs/:id/approve` `{reviewerHint?}` | Move run from `awaiting_review` → `approved`, insert a pending `evolution_applies` row that the ticker will pick up. |
317
319
  | POST | `/api/evolution/runs/:id/reject` | Move run from `awaiting_review` → `rejected`. |
318
320
  | POST | `/api/evolution/runs/:id/retry` `{hint}` | Reset a finished run back to `pending` with the supplied (required) hint so the wrapper picks it up again. Rejected with 409 when the run is already `running` / `pending`. |
@@ -1,9 +1,12 @@
1
1
  # Desktop Packaging (Electron / DMG)
2
2
 
3
- How to build the macOS desktop app (`Halo-0.1.5-arm64.dmg`). The desktop
4
- build is a thin Electron shell (`packages/desktop`) that bundles a self-
5
- contained server runtime + a private `node` binary + the admin static export,
6
- then spawns the server as a child process and points a `BrowserWindow` at it.
3
+ How to build the desktop app. We ship two artifacts: **Windows x64**
4
+ (`Halo Setup <ver>.exe`, built on a Linux host) and **macOS arm64**
5
+ (`Halo-<ver>-arm64.dmg`, built on Apple Silicon) — see the target matrix
6
+ below. The desktop build is a thin Electron shell (`packages/desktop`) that
7
+ bundles a self-contained server runtime + a private `node` binary + the admin
8
+ static export, then spawns the server as a child process and points a
9
+ `BrowserWindow` at it.
7
10
 
8
11
  See [design](../design/) for the server itself; this doc is only about the
9
12
  packaging pipeline.
@@ -36,6 +39,13 @@ one.
36
39
 
37
40
  ```bash
38
41
  cd packages/halo # repo root
42
+ # 0. VERSION GATE — confirm every package is on the release version BEFORE
43
+ # building or tagging. The five packages must move in lockstep; a single
44
+ # lagging one ships a mismatched build and a wrong `halo --version`.
45
+ for f in packages/{core,server,admin,cli,desktop}/package.json; do
46
+ printf "%-32s " "$f:"; grep -m1 '"version"' "$f"
47
+ done # all five must read the same x.y.z, and match the tag you're about to cut
48
+
39
49
  # 1. build the upstream artifacts the stage script consumes, IN ORDER.
40
50
  # - core MUST build before server: server imports @turmind/halo-core
41
51
  # (e.g. GitManager in routes/git.ts). A stale/missing core/dist makes
@@ -46,6 +56,10 @@ cd packages/halo # repo root
46
56
  pnpm --filter @turmind/halo-core build
47
57
  pnpm --filter @turmind/halo-server build
48
58
  pnpm --filter @turmind/halo-admin build
59
+ # NOTE: acp-adapter is NOT listed here on purpose — build-bundle.mjs now
60
+ # builds every workspace:* dep (core / server / acp-adapter) itself before
61
+ # esbuild inlines them, so its dist can't go stale. This block only covers
62
+ # artifacts the stage script consumes directly (server dist, admin-out).
49
63
 
50
64
  # 2. stage + package (arm64)
51
65
  cd packages/desktop
@@ -58,8 +72,47 @@ Output: `packages/desktop/dist/Halo-0.1.5-arm64.dmg` (~143 MB).
58
72
  `gen-icon` → `prepack-dmg` (`scripts/stage-runtime.mjs`) → `electron-builder
59
73
  --mac --arm64`.
60
74
 
61
- Other targets: `pnpm dist:x64` (Intel mac), `pnpm dist:win` (Windows nsis,
62
- cross-staged from mac).
75
+ ## Release target matrix (current policy)
76
+
77
+ Two shipped artifacts per release, each built on its native-ish host:
78
+
79
+ | Target | Host we build on | Command | Notes |
80
+ |--------|------------------|---------|-------|
81
+ | **Windows x64** (`Halo Setup <ver>.exe`, nsis) | **Linux x86_64** | `HALO_STAGE_FULL=1 CI=true pnpm dist:win` | host arch == target arch (x64), so native fixups are low-risk. This is now the standard path — *not* cross-staged from mac. |
82
+ | **macOS arm64** (`Halo-<ver>-arm64.dmg`) | macOS (Apple Silicon) | `CI=true pnpm dist:arm64` | Apple Silicon only. |
83
+
84
+ **`pnpm dist:x64` (Intel mac) is retired — we no longer ship an Intel build.**
85
+ The `dist:x64` script still exists in `package.json` but is not part of any
86
+ release; don't cut one.
87
+
88
+ ### Building the Windows exe on Linux (the standard path)
89
+
90
+ ```bash
91
+ # from repo root — build upstream artifacts in order (see step 1 below), then:
92
+ cd packages/desktop
93
+ HALO_STAGE_FULL=1 CI=true pnpm dist:win
94
+ ```
95
+
96
+ `HALO_STAGE_FULL=1` is **mandatory** here: a cross-stage must re-run the
97
+ per-target native fixups (`@parcel/watcher-win32-x64` swap, `better-sqlite3`
98
+ win prebuild), which the fast path skips. `wine` must be installed (electron-
99
+ builder runs the nsis maker through it).
100
+
101
+ Verified on the v0.1.9 build (Linux x86_64 host → win-x64):
102
+ - **No poisoned arm64 dmg to discard.** Unlike a mac host, a Linux `--win` run
103
+ never triggers the yml's mac target — `dist/` contains only `Halo Setup
104
+ <ver>.exe` + `.exe.blockmap` + `win-unpacked/`. The "discard the stray
105
+ arm64 dmg" gotcha below is mac-host-only.
106
+ - **Native arch check** — every staged `.node` must be PE32+ x86-64, no ELF:
107
+ `find dist/win-unpacked -path '*watcher-*' -name '*.node' | xargs file` and
108
+ `file dist/win-unpacked/resources/node.exe` (should read `PE32+ ... x86-64`).
109
+ - **Two benign warnings**, both expected: `file source doesn't exist
110
+ from=.../resources/node` (win bundles `node.exe`, not the Unix `node` — the
111
+ yml lists both, electron-builder skips the absent one) and `signing is
112
+ skipped` (no Windows code-sign cert). Neither fails the build.
113
+
114
+ > A `desktop-packaging.local.md` (gitignored, per-host) may carry extra
115
+ > machine-specific setup — read it first if present.
63
116
 
64
117
  ## Faster rebuilds
65
118
 
@@ -138,8 +191,10 @@ Still good practice: build server + admin **before** `pnpm dist:arm64`.
138
191
  2. **Copy `templates/`** — `pnpm deploy` doesn't pick these up; copied
139
192
  explicitly. This is what carries the built-in skills (incl. `send-file`),
140
193
  agents, prompts, models into the dmg.
141
- 3. **Cross-arch native fixup** — when target arch/platform ≠ host, re-fetch
142
- `better-sqlite3` prebuilds via `prebuild-install`.
194
+ 3. **Native fixup** — re-fetch `better-sqlite3` prebuilds via `prebuild-install
195
+ --target=22.11.0` **unconditionally** (pins the ABI to the bundled node
196
+ regardless of the build host's node — see the ABI gotcha below);
197
+ `@parcel/watcher`'s per-platform binary is swapped only when cross-staging.
143
198
  4. **Trim bloat** — drop non-target `node-pty` prebuilds, `better-sqlite3`'s
144
199
  C source, and the server's `src/`/`tests/`.
145
200
  5. **Copy `admin-out/`** — the Next.js static export.
@@ -147,6 +202,9 @@ Still good practice: build server + admin **before** `pnpm dist:arm64`.
147
202
  nodejs.org, extract the `node` binary into `resources/`.
148
203
  7. **codesign (ad-hoc)** — `codesign --force --sign -` on the node binary for
149
204
  Gatekeeper. **We deliberately do NOT `strip`** — see gotcha below.
205
+ 8. **Native-addon smoke test** — load the staged better-sqlite3 with the
206
+ bundled node binary; abort the build if it can't `dlopen` (the ABI backstop,
207
+ runs on full and fast paths). See the ABI gotcha below.
150
208
 
151
209
  ## Gotchas (learned the hard way)
152
210
 
@@ -178,6 +236,28 @@ Still good practice: build server + admin **before** `pnpm dist:arm64`.
178
236
  looks fine but won't boot. Keep the full ~114 MB binary; the ~30 MB saved
179
237
  isn't worth a dead app. (`stage-runtime.mjs` has the strip step removed.)
180
238
 
239
+ - **better-sqlite3's ABI must match the BUNDLED node (22.11.0 / ABI 127), not
240
+ the build host's node.** `pnpm deploy` / `npm install` hard-link whatever
241
+ `.node` the build host's *active* node produced. If a homebrew/nvm node 23
242
+ (ABI 131) shadows the pinned 22 on the build machine, the staged
243
+ `better_sqlite3.node` is ABI 131 — but the app runs it on the bundled node 22,
244
+ so it dies at launch with `NODE_MODULE_VERSION 131 ... requires 127`
245
+ (`ERR_DLOPEN_FAILED`, server `code=1`). The dmg packages fine and runs on the
246
+ builder's own machine (whose node happens to match), so it ships broken to
247
+ everyone else. Three guards now prevent this, all in `stage-runtime.mjs`:
248
+ 1. `fetchTargetArchNatives()` runs `prebuild-install --target=22.11.0` for
249
+ better-sqlite3 **unconditionally** (not just when cross-staging), so the
250
+ ABI is always re-pinned to the bundled node regardless of the host node.
251
+ 2. An early gate aborts if the build host's node major ≠ the bundled major
252
+ (`nvm use 22` then rebuild; override with `HALO_SKIP_NODE_CHECK=1`).
253
+ 3. `smokeTestNativeAddons()` loads the staged better-sqlite3 with the bundled
254
+ node binary at the end of every stage (full AND fast) and aborts the build
255
+ if it can't `dlopen` — the final backstop no matter how a bad ABI crept in
256
+ (wrong host node, a fast path reusing a stale tree, a future native dep).
257
+ Verify a built dmg manually: `"<app>/Contents/Resources/node" -e
258
+ "new (require('<app>/Contents/Resources/server-runtime/node_modules/better-sqlite3'))(':memory:')"`
259
+ should exit 0.
260
+
181
261
  - **Desktop port is sequential from 9527 (NOT random), and that's load-bearing
182
262
  for persistence.** `main.cjs` `findFreePort()` tries 9527 first, then scans
183
263
  upward for the first free one, and passes it to the server child. It used to
@@ -236,6 +316,16 @@ Still good practice: build server + admin **before** `pnpm dist:arm64`.
236
316
  with `&& node scripts/copy-monaco.mjs`. If you must run `next build`
237
317
  directly, append the copy step yourself.
238
318
 
319
+ - **All five package versions must move in lockstep — check before building or
320
+ tagging.** `core` / `server` / `admin` / `cli` / `desktop` each carry their own
321
+ `version` in `package.json`, and nothing auto-syncs them. When cutting 0.1.9,
322
+ `cli` + `desktop` were already bumped by hand but `server` sat at 0.1.8 and
323
+ `core` / `admin` at 0.1.0 — a `git tag` then would have shipped a build whose
324
+ `halo --version` (stamped from `package.json`) disagreed with the tag. The
325
+ step-0 version gate in "One-shot build" loops over all five so a lagging one is
326
+ caught before the tag is cut. (npm only publishes `cli` as `@turmind/halo`, but
327
+ the others feed `halo --version` / the admin sidebar, so they still matter.)
328
+
239
329
  - **Changing anything under `templates/` requires bumping `TEMPLATE_VERSION`
240
330
  (`packages/server/src/init.ts`) — otherwise the change never reaches existing
241
331
  installs.** Server startup only re-runs `ensureHaloHome` (the template
@@ -87,7 +87,7 @@ Source: `packages/server/src/config.ts`
87
87
  | Variable | Default | Config path | Description |
88
88
  |---|---|---|---|
89
89
  | `HALO_PORT` | `9527` | `config.yaml server.port` | Hono listen port |
90
- | `HALO_PASSWORD` | (none) | `config.yaml server.password` (scrypt hash) | Plaintext login password. When set, takes precedence over the stored hash and bypasses scrypt entirely — intended for Docker / systemd / CI. The hash is set by `halo setup` for interactive installs. |
90
+ | `HALO_PASSWORD` | (none) | `config.yaml server.password` (scrypt hash) | Plaintext login password. When set, takes precedence over the stored hash and bypasses scrypt entirely — intended for Docker / systemd / CI. It also satisfies the **startup gate**: the server refuses to boot with no password configured, and env plaintext counts as a first-class credential there, so an env-only deployment (`halo setup -y && HALO_PASSWORD=... halo server start`, no stored hash) boots fine. The hash is set by `halo setup` for interactive installs. |
91
91
  | `HALO_CORS_ORIGINS` | empty (reflect any origin) | `config.yaml server.cors_origins` | CORS allowlist (comma-separated). Empty = reflect any incoming Origin so credentials work cross-origin. Set explicit list to enforce strict CORS. |
92
92
  | `HALO_FRONTEND_DIR` | `packages/admin/out` | — | Static frontend dir (resolved as absolute path from project root) |
93
93
  | `HALO_MAX_CONTEXT_TOKENS` | `200000` | — | Model max context |
@@ -117,6 +117,8 @@ settings.yaml only (no env override):
117
117
  - `kimi.secrets.api_key` — Kimi (Moonshot AI)
118
118
  - `deepseek.secrets.api_key` — DeepSeek
119
119
 
120
+ When a provider manifest declares a `default: <<NAME>>` for a secret, `halo setup` offers a "Use env $NAME" action. Picking it **writes the literal `<<NAME>>` placeholder into settings.yaml**, which the standard env-var interpolation ([storage.md](../design/storage.md#env-var-interpolation-env_name)) expands against `process.env` at read time. There is no separate runtime env fallback — the placeholder in the file is the only mechanism (an unset env var leaves the literal `<<NAME>>` visible in the request, failing loudly).
121
+
120
122
  `<skill-id>.params.*` (referenceable from skills via `{{<skill-id>.params.<key>}}`, or short form `{{params.<key>}}` inside SKILL.md; declared in `skills/<id>/config.yaml`):
121
123
  - `tavily-search.params.api_key` — example only; declared by whichever skill needs it
122
124
 
@@ -136,7 +138,7 @@ settings.yaml-driven `general.limits.*` (no env override):
136
138
  Two ways to authenticate:
137
139
 
138
140
  1. **Interactive** — run `halo setup`, pick "Set / change password". The value is scrypt-hashed and written to `~/.halo/secrets/config.yaml server.password`. Best for personal installs.
139
- 2. **Env** — export `HALO_PASSWORD=...` before launching the server. The plaintext value is compared directly (no hashing) and takes precedence over any stored hash. Best for Docker / systemd / CI where a secrets manager already protects the env.
141
+ 2. **Env** — export `HALO_PASSWORD=...` before launching the server. The plaintext value is compared directly (no hashing) and takes precedence over any stored hash. A stored hash isn't required at all — env plaintext alone satisfies the startup gate, so env-only deployments are legal. Best for Docker / systemd / CI where a secrets manager already protects the env.
140
142
 
141
143
  ## Verify
142
144
 
@@ -27,6 +27,9 @@ previews/
27
27
  ├── docx.tsx / docx-view.tsx
28
28
  ├── xlsx.tsx / xlsx-view.tsx
29
29
  ├── pptx.tsx / pptx-view.tsx
30
+ ├── pptx-notes.ts pptx zip helpers: speaker-notes extraction (jszip +
31
+ │ DOMParser, play order via sldIdLst) + Content_Types
32
+ │ repair for decks with dangling Override parts
30
33
  └── media.tsx / media-view.tsx
31
34
  ```
32
35
 
@@ -181,6 +181,8 @@ Start a sub-agent session asynchronously. When the sub-agent finishes, its **wra
181
181
  - Depth exceeded: `{"code": 1, "error": "Maximum nesting depth (N) reached..."}`
182
182
  - Working dir invalid: `{"code": 1, "error": "working_dir \"...\" is outside the workspace"}` (or does-not-exist / not-a-directory)
183
183
 
184
+ UI note: the `agent_start` event this emits carries both a 200-char `text` preview (parent-side rendering) and the full un-truncated brief as `fullText`, which seeds the sub-session log's opening user message — so viewing the child session shows the complete task brief (`system_prompt_context` + `message`), not a cut-off preview. See [session.md → Event routing](../design/session.md#event-routing).
185
+
184
186
  ### session_list
185
187
 
186
188
  List direct child sessions of the current session and their status.
@@ -293,7 +295,7 @@ Returns: full SKILL.md content (body + resource files list). For progressive dis
293
295
  |---|---|---|---|
294
296
  | `content` | string | yes | The complete draft answer. Required and must be non-empty |
295
297
 
296
- The tool **description is deliberately plain** — "Submit a draft answer for self-review; returns a checklist that critiques the draft." It does NOT say *when* to call it (no "call this for complex questions / when unsure"). An earlier version that did embed such self-referential guidance made some models (notably GPT-5.x) thrash. Steering toward *using* draft belongs in the agent's `system_prompt` (e.g. the bundled `default` agents say "hold your answers to a high standard… don't reply off the cuff"), not in the tool description. Empty `content` is rejected with an error **without consuming a draft round**.
298
+ The tool **description is deliberately plain** — "Submit a draft answer for self-review; returns a checklist that critiques the draft." It does NOT say *when* to call it (no "call this for complex questions / when unsure"). An earlier version that did embed such self-referential guidance made some models (notably GPT-5.x) thrash. Steering toward *using* draft belongs in the agent's prompt files (the bundled `default` agent's AGENT.md says "hold your answers to a high standard… don't reply off the cuff" — it lives in AGENT.md because the yaml `system_prompt` is only a fallback when the MD layer is empty), not in the tool description. Empty `content` is rejected with an error **without consuming a draft round**.
297
299
 
298
300
  **Why it exists:** the agent loop only makes another model call when the model emits a `tool_use` block. A plain-text answer (`end_turn`) is single-pass — `thinking` runs *before* the answer in the same call, so the model never gets to look at its *finished* answer and revise it. `draft` closes that gap without touching the loop: the model writes its answer into `content` (materialised into the conversation as a `tool_use` block — uncapped, never echoed back), and the tool_result hands back an adversarial review checklist (framed as a hostile reviewer: list every factual claim and tag its source, verify the unverified ones with tools *now*, check directness/tone, flag gaps). The next model call then critiques that now-concrete draft and either revises (calls `draft` again) or writes the final answer.
299
301
 
@@ -46,12 +46,14 @@ End users then install with `npm install -g @turmind/halo`.
46
46
  Run a one-shot prompt and exit:
47
47
 
48
48
  ```bash
49
- halo "review this PR"
50
- halo -w /path/to/workspace "analyze code"
51
- echo "review the changes" | halo
52
- cat prompt.txt | halo -w /path/to/review-ws
49
+ halo cli "review this PR"
50
+ halo cli -w /path/to/workspace "analyze code"
51
+ echo "review the changes" | halo cli
52
+ cat prompt.txt | halo cli -w /path/to/review-ws
53
53
  ```
54
54
 
55
+ To list agents or sessions without running a prompt, use the dedicated subcommands `halo agents` / `halo sessions` (same `-w` flag).
56
+
55
57
  ### Options
56
58
 
57
59
  | Flag | Short | Default | Description |
@@ -59,11 +61,10 @@ cat prompt.txt | halo -w /path/to/review-ws
59
61
  | `--workspace` | `-w` | cwd | Workspace path |
60
62
  | `--agent` | `-a` | `default` | Agent ID |
61
63
  | `--session` | `-s` | auto | Resume session by ID |
64
+ | `--new` | `-n` | off | Always start a new session |
62
65
  | `--format` | `-f` | `text` | Output format: `text` or `json` |
63
66
  | `--verbose` | `-v` | off | Show thinking, tool calls, usage on stderr |
64
67
  | `--access` | | `full` | Access level: `full`, `workspace`, `readonly` |
65
- | `--agents` | | | List available agents and exit |
66
- | `--sessions` | | | List recent sessions and exit |
67
68
  | `--lang` | | `en` | Language: `en` or `zh` |
68
69
 
69
70
  ### Output
@@ -91,21 +92,22 @@ cat prompt.txt | halo -w /path/to/review-ws
91
92
 
92
93
  ```bash
93
94
  # Code review in CI
94
- halo -w /path/to/review-ws "review the diff" --format json | jq '.text'
95
+ halo cli -w /path/to/review-ws "review the diff" --format json | jq '.text'
95
96
 
96
97
  # Pipe content in
97
- git diff HEAD~1 | halo -w /path/to/review-ws "review this diff"
98
+ git diff HEAD~1 | halo cli -w /path/to/review-ws "review this diff"
98
99
 
99
100
  # Exit code: 0 = success, 1 = error, 130 = SIGINT
100
- halo "check for issues" && echo "OK" || echo "FAILED"
101
+ halo cli "check for issues" && echo "OK" || echo "FAILED"
101
102
  ```
102
103
 
103
104
  ## TUI Mode (interactive)
104
105
 
105
106
  ```bash
106
- halo -i
107
- halo -i -w /path/to/workspace
108
- halo -i -s sid_abc123 # resume session
107
+ halo tui
108
+ halo tui -w /path/to/workspace
109
+ halo tui -s sid_abc123 # resume session
110
+ halo tui -n # always start a new session
109
111
  ```
110
112
 
111
113
  Multi-turn conversation. Supports all standard Halo slash commands:
@@ -130,6 +132,16 @@ Multi-turn conversation. Supports all standard Halo slash commands:
130
132
  | `/evo [hint]` | Queue a self-evolution run on this session (full access only) |
131
133
  | `/quit` | Exit |
132
134
 
135
+ TUI-only client commands (handled locally, also visible in the completion popup):
136
+
137
+ | Command | Description |
138
+ |---|---|
139
+ | `/clear` | Start a new session (alias for `/session new`) |
140
+ | `/retry` | Resend the last user message |
141
+ | `/verbose` | Toggle verbose tool output (args + truncated results) at runtime — same as launching with `-v`; the status bar shows a `v` badge while on |
142
+ | `/log` | Browse the session tree and view a session log (same as `Ctrl+O`) |
143
+ | `/exit` | Alias for `/quit` |
144
+
133
145
  Bare `/<obj>` (or `/<obj> help`) lists the verbs you may run. Verbs gated above your access level are hidden.
134
146
 
135
147
  ### Keybindings
@@ -139,9 +151,15 @@ Bare `/<obj>` (or `/<obj> help`) lists the verbs you may run. Verbs gated above
139
151
  | `Esc` (while running) | Interrupt the current turn immediately — aborts a command mid-execution, then any messages typed while it was running are folded into one follow-up turn. Same as `/session interrupt`. |
140
152
  | `Ctrl+C` | Graceful exit; press twice to force |
141
153
  | `Ctrl+O` | Toggle the sub-agent navigator — lists every sub-agent spawned this session, each showing its agent name, task title (same as the session list), and status (`●` running green / `○` idle grey / `✕` stopped red, plus a `▢ archived` marker when the session is archived); `↑↓` to move, `Enter` to view that sub-agent's log, `Esc`/`q` to close. (Was `Shift+Tab`, but Windows terminals consume that as backtab.) The log viewer auto-refreshes while the viewed session is still running (a `● live` hint shows in the header) and follows the bottom as new output lands — unless you've scrolled up to read, in which case it stays put (`G` jumps back to the bottom and resumes following). |
142
- | `↑` / `↓` | Walk input history (when no popup is open) |
154
+ | `↑` / `↓` | Walk input history (when no popup is open). History persists across restarts in `~/.halo/global/tui-history.json` (last 100 entries, shared across workspaces) |
155
+ | `←` / `→`, `Home` / `End` | Move the cursor within the input; text is inserted/deleted at the cursor (CJK and emoji safe) |
156
+ | `Ctrl+A` / `Ctrl+E` | Jump to start / end of input |
157
+ | `Ctrl+W` / `Alt+Backspace` | Delete the word before the cursor |
158
+ | `Ctrl+U` / `Ctrl+K` | Delete to start / to end of input |
159
+
160
+ On resume (`-s <id>`, or the default latest session), the TUI replays the session's prior conversation on screen so you see where you left off. `shell_exec` output is shown inline by default (other tools' output needs `-v`). Tool lines show a short argument summary and duration, e.g. `⚙ file_read hello.txt 4ms`.
143
161
 
144
- On resume (`-s <id>`, or the default latest session), the TUI replays the session's prior conversation on screen so you see where you left off. `shell_exec` output is shown inline by default (other tools' output needs `-v`).
162
+ **Multi-line paste**: pasting text with newlines (or >800 chars) collapses into a compact placeholder like `[#1 pasted 3 lines]` in the input; you can keep typing around it, and the original text is expanded back in full on submit. Pasted content is inserted at the cursor position.
145
163
 
146
164
  ## File, Image, and Scope References
147
165
 
@@ -174,10 +192,16 @@ Attach files or images to your message with `@file` and `@image`, or pull a dire
174
192
  ```
175
193
  packages/cli/
176
194
  src/
177
- index.ts — Entry: arg parsing, mode dispatch, SIGINT handling
195
+ index.ts — Entry: subcommand dispatch (tui/cli/agents/sessions/server/setup/acp), SIGINT handling
178
196
  harness.ts — Shared agent harness wrapping SessionManager
179
197
  cli.ts — Non-interactive: stdin/args → agent → stdout → exit
180
- tui.ts — Interactive: readline loop + event display + commands
198
+ tui.tsx — Interactive entry: mounts the ink app
199
+ tui/
200
+ app.tsx — Root component: event reducer, blocks, submit/command handling
201
+ line-editor.ts — Grapheme-aware pure line-editing functions (cursor, insert, delete)
202
+ history.ts — Persistent input history (~/.halo/global/tui-history.json)
203
+ components/ — input-box, text-input, messages, streaming, status-bar,
204
+ slash-suggest, path-suggest, log-navigator, log-viewer, banner
181
205
  format-usage.ts — Usage-line formatter (mirrors admin UsageLine badge order)
182
206
  resolve-refs.ts — @file / @image reference resolution with size limits
183
207
  render-md.ts — Terminal markdown rendering (marked + marked-terminal)
@@ -6,8 +6,9 @@ What to do the first time you open Halo.
6
6
 
7
7
  Open `http://localhost:9527` (or your deployment URL) in a browser and enter the password.
8
8
 
9
- - The default password is printed to the **server console** (auto-generated if you haven't set `HALO_PASSWORD`)
10
- - You can also pin a password via `server.password` in `~/.halo/secrets/config.yaml`
9
+ - Set the password during `halo setup` (interactive) — it's stored as a scrypt hash in `~/.halo/secrets/config.yaml`
10
+ - Or supply it via the `HALO_PASSWORD` env var (handy for CI / Docker), which takes precedence over the stored hash
11
+ - If neither is set, the server refuses to start (it prints `admin password not configured` and exits)
11
12
 
12
13
  ## Open a workspace
13
14
 
@@ -42,7 +42,7 @@ Non-text files go through `FilePreview`, which looks up a **plugin** in the prev
42
42
  - **PDF** — browser-native iframe
43
43
  - **DOCX / DOC** — `mammoth` → HTML (parsed in a Web Worker so the UI stays responsive)
44
44
  - **XLSX / XLS / CSV** — `xlsx` (SheetJS) → table (parsed in a Web Worker; CSV decoded as UTF-8 with BOM strip)
45
- - **PPTX / PPT** — `pptx-preview` list mode. Fidelity is approximate — complex animations / SmartArt / embedded fonts may not render perfectly; users can download for the exact source. Flagged `heavy: true` so only the *active* pptx mounts (canvas-based rendering needs the main thread)
45
+ - **PPTX / PPT** — `pptx-preview` list mode. Fidelity is approximate — complex animations / SmartArt / embedded fonts may not render perfectly; users can download for the exact source. Flagged `heavy: true` so only the *active* pptx mounts (canvas-based rendering needs the main thread). **Speaker-notes sidebar**: notes are extracted from the pptx zip in presentation (play) order and listed on the left; clicking a note scrolls to the corresponding slide (a domIndex mapping bridges play order to pptx-preview's part-filename render order, so reordered decks still land on the right slide). Collapsible with the state remembered in localStorage (`halo.pptxNotesHidden`); the sidebar doesn't appear at all when no slide has notes. Decks with dangling `[Content_Types].xml` overrides (e.g. some WPS exports) are repaired before rendering; if zero slides render anyway, a real error UI is shown instead of a silent black wrapper
46
46
  - **Images / video / audio** — native `<img>` / `<video>` / `<audio>`, supports HTTP Range for seek-without-full-download
47
47
 
48
48
  All previews share a **standard header** (`PreviewShell`) with filename, Download, Open-as-Text, and an `extraToolbar` slot for plugin-specific buttons (e.g. DOCX Print, XLSX sheet tabs).
@@ -12,6 +12,16 @@ VS Code-style file tree sidebar with multi-select, drag-drop, right-click menu.
12
12
  ### Path input
13
13
  - Explorer top bar has a path input + 📁🔍 picker button
14
14
  - Pressing Enter calls `GET /api/fs/exists?path=...` first — a miss shows an alert, no switch
15
+ - **Recent-workspaces dropdown**: focusing the input shows the most-recently-used list; typing filters it by substring. Each row shows the folder name + full path; clicking one switches to it; a hover "×" removes a single entry. Entries are recorded only after a switch validates the path (so invalid paths never land), in canonical resolved form, MRU-ordered and deduped, capped at 8. Persisted in localStorage (`halo_recent_workspaces`)
16
+
17
+ ### Desktop windows (multi-window)
18
+ Desktop app only — the browser has one tab per window natively.
19
+ - **New window**: `Cmd+N` (macOS, via the File menu) / `Ctrl+N` (Windows/Linux, bound at the webContents level since those platforms have no app menu). Each window is a fresh view onto the **one shared local server** — no second server process spawns.
20
+ - **Per-window workspace**: windows share the server origin, hence one localStorage, so a window tracks its own workspace via its URL `?folder=` (not localStorage, which would clobber across windows). Different windows can sit on different workspaces; the same workspace can be open in two windows.
21
+ - **Quit semantics** (platform split): on macOS, closing every window keeps the app in the Dock (only `Cmd+Q` truly exits) and clicking the Dock icon reopens a window; on Windows/Linux there's no Dock to resummon from, so closing the last window quits.
22
+ - **Pin (always-on-top)** acts per-window — each window pins independently.
23
+ - **Activity awareness**: while the agent streams a reply the sidebar's workspace name carries an amber pulsing dot (emerald when idle) and the window title gains a `●` prefix (`● Halo — <name>` busy vs `Halo — <name>` idle) — both are plain web behaviour that Electron mirrors to the native title bar.
24
+ - **Finish notification** (works in both the desktop app and a plain browser): an opt-in bell toggle sits in the activity bar next to the pin, **off by default**, persisted per-machine in localStorage (`halo_notify_on_finish`). It's shown whenever a notification can be raised — the desktop shell (`window.haloNotify` bridge) or a browser with the Web Notification API; turning it on in a browser requests Notification permission from within the toggle click (the required user gesture). When on, the busy→idle edge of the *same* still-open session triggers two decoupled cues: a **self-synthesized WebAudio chime** that always plays (regardless of focus — an audible "it's done" even while you're watching the tab, and not subject to the OS per-site notification-sound setting), plus — **only when the window is unfocused** — a banner (desktop: native notification + Dock bounce / taskbar flash; browser: a Web Notification whose click refocuses the tab). Focused → chime only, no banner.
15
25
 
16
26
  ### Directory picker (FolderPicker modal)
17
27
  Visual directory browser opened by the 📁🔍 button:
@@ -54,6 +64,7 @@ Items shown depend on the click target:
54
64
  |---|---|---|
55
65
  | New File / New Folder | Always | `POST /api/files/new` · `mkdir`; auto-expands the parent folder so the inline input is visible |
56
66
  | Open in Integrated Terminal | Always | Spawns a terminal at the target dir (or file's parent) |
67
+ | Reveal in File Manager | Desktop app only | Opens the OS file manager (Finder / Explorer / Linux FM) at the target — a folder opens itself, a file is highlighted in its parent dir. Hidden in a plain browser (gated on the `window.haloReveal` IPC bridge exposed by the desktop preload) |
57
68
  | Open to the Side | File only | Splits the editor and opens the file in the right pane |
58
69
  | Download | File only | `GET /api/files/download?path=...` |
59
70
  | Rename | Single file/folder | `POST /api/files/rename` |