wezard 1.4.1 → 2.0.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.
Files changed (71) hide show
  1. package/CLAUDE.md +12 -7
  2. package/README.md +227 -112
  3. package/cli/wezard.sh +6 -3
  4. package/dist/daemon/approval.js +9 -3
  5. package/dist/daemon/approval.js.map +1 -1
  6. package/dist/daemon/cc-bridge.js.map +1 -1
  7. package/dist/daemon/chat-name.js +8 -14
  8. package/dist/daemon/chat-name.js.map +1 -1
  9. package/dist/daemon/detail.js +94 -9
  10. package/dist/daemon/detail.js.map +1 -1
  11. package/dist/daemon/graph.js +5 -7
  12. package/dist/daemon/graph.js.map +1 -1
  13. package/dist/daemon/inbound.js +135 -158
  14. package/dist/daemon/inbound.js.map +1 -1
  15. package/dist/daemon/index.js +452 -156
  16. package/dist/daemon/index.js.map +1 -1
  17. package/dist/daemon/jobs.js +2 -2
  18. package/dist/daemon/mirror-bridge.js +284 -140
  19. package/dist/daemon/mirror-bridge.js.map +1 -1
  20. package/dist/daemon/mirror-store.js.map +1 -1
  21. package/dist/daemon/model-select.js +74 -0
  22. package/dist/daemon/model-select.js.map +1 -0
  23. package/dist/daemon/peers.js +10 -16
  24. package/dist/daemon/peers.js.map +1 -1
  25. package/dist/daemon/spawn-tmux.js +27 -4
  26. package/dist/daemon/spawn-tmux.js.map +1 -1
  27. package/dist/daemon/task-gate.js +97 -0
  28. package/dist/daemon/task-gate.js.map +1 -0
  29. package/dist/daemon/task-registry.js +132 -0
  30. package/dist/daemon/task-registry.js.map +1 -0
  31. package/dist/daemon/tasks.js +126 -54
  32. package/dist/daemon/tasks.js.map +1 -1
  33. package/dist/daemon/wizard-memory.js +57 -0
  34. package/dist/daemon/wizard-memory.js.map +1 -0
  35. package/dist/daemon/wizard.js +84 -36
  36. package/dist/daemon/wizard.js.map +1 -1
  37. package/dist/mcp/server.js +138 -76
  38. package/dist/mcp/server.js.map +1 -1
  39. package/dist/shared/chat-http.js +193 -96
  40. package/dist/shared/chat-http.js.map +1 -1
  41. package/dist/shared/chat-render.js +31 -24
  42. package/dist/shared/chat-render.js.map +1 -1
  43. package/dist/shared/chat-view.js +14 -2
  44. package/dist/shared/chat-view.js.map +1 -1
  45. package/dist/shared/config.js +23 -4
  46. package/dist/shared/config.js.map +1 -1
  47. package/dist/shared/detail-render.js +14 -7
  48. package/dist/shared/detail-render.js.map +1 -1
  49. package/dist/shared/detail-store.js +12 -5
  50. package/dist/shared/detail-store.js.map +1 -1
  51. package/dist/shared/role-render.js +52 -0
  52. package/dist/shared/role-render.js.map +1 -0
  53. package/dist/shared/role-view.js +210 -0
  54. package/dist/shared/role-view.js.map +1 -0
  55. package/dist/shared/schedule-spec.js +92 -116
  56. package/dist/shared/schedule-spec.js.map +1 -1
  57. package/dist/shared/session-label.js +103 -72
  58. package/dist/shared/session-label.js.map +1 -1
  59. package/dist/shared/std.js +20 -0
  60. package/dist/shared/std.js.map +1 -1
  61. package/dist/shared/task-file.js +120 -0
  62. package/dist/shared/task-file.js.map +1 -0
  63. package/dist/shared/trigger.js +232 -0
  64. package/dist/shared/trigger.js.map +1 -0
  65. package/dist/shared/world.js +193 -0
  66. package/dist/shared/world.js.map +1 -0
  67. package/dist/svr/index.js +68 -3
  68. package/dist/svr/index.js.map +1 -1
  69. package/package.json +1 -1
  70. package/web/chat.css +609 -147
  71. package/web/chat.js +1274 -215
package/CLAUDE.md CHANGED
@@ -54,15 +54,20 @@ Three processes, all coordinating via 127.0.0.1:17890 and `~/.wezard/`:
54
54
  - `approval.ts` builds the button card, parks an HTTP request via `pending.ts`, and resolves it on `event.template_card_event`. Three click outcomes: `allow` / `allow_window` (auto-allow N min for that session) / `deny`. `session-cache.ts` short-circuits repeats.
55
55
  - `inbound.ts` routes `message.text` / `message.image` / `message.mixed` → either headless `cc-bridge.ts` or `mirror-bridge.ts`. Gates on `allowFrom`; one-shot exception is the bootstrap claim (see below).
56
56
  - `cc-bridge.ts` (mode=headless) spawns `claude -p --output-format stream-json --verbose`, parses `assistant`/`stream_event` lines, and pumps text via `client.replyStream`. Per-principal queue serializes turns; session id is captured from the `system/init` line and persisted to `~/.wezard/sessions.json` for `--resume`.
57
- - `mirror-bridge.ts` (mode=mirror) tails `<projectsDir>/<encoded(cwd)>/<sid>.jsonl` and pushes new `assistant` content to a chat — but only for turns the chat itself started: a turn opened by a human's CLI keystroke is recorded to the turn store (chat-detail page) and kept out of the chat (`wrc.mirror.chatOriginOnly`, brief mode only); approval/askq cards and `[mirror]` notices bypass that gate. Inbound injection is either `claude --resume <sid> -p` (writes a turn into the same jsonl) or `tmux paste-buffer` against a live TTY (only path that makes WeCom messages visible in the user's CLI window). `recentInjects` ring buffer suppresses the WeCom→CLI echo when tailing.
57
+ - `mirror-bridge.ts` (mode=mirror) tails `<projectsDir>/<encoded(cwd)>/<sid>.jsonl` and pushes new `assistant` content to a chat — but only for turns the chat itself started: a turn opened by a human's CLI keystroke is recorded to the turn store (rolepage) and kept out of the chat (`wrc.mirror.chatOriginOnly`, brief mode only); approval/askq cards and `[mirror]` notices bypass that gate. Inbound injection is either `claude --resume <sid> -p` (writes a turn into the same jsonl) or `tmux paste-buffer` against a live TTY (only path that makes WeCom messages visible in the user's CLI window). `recentInjects` ring buffer suppresses the WeCom→CLI echo when tailing (which is why an injected line's text is stamped as the turn's `userQuery` at inject time — the tail never sees it). **Every turn has a channel** (`TurnDetailRecord.channel`): the base of the public chat it was spoken in and where its reply goes — a human can `.name` a wizard homed in another chat and the reply (text and cards) returns to the chat that asked, not the wizard's home; `""` = a private wizard↔wizard turn whose reply is pushed nowhere and lives only on the rolepage.
58
58
  - `claim.ts` is a single-shot bootstrap: an exact-match magic phrase from any sender bypasses `allowFrom`, writes that principal as `defaultChat`, and appends to `allowFrom`. This is the **only** legitimate ingress for first-time setup.
59
- - `peers.ts` is the read side of multi-agent awareness — "who else is working in this chat, and are they busy". The wizards of one chat are exactly the target keys sharing a base principal (`baseOfKey`): the untagged default plus every `#tag`. Pure parsing only — bounded transcript-tail reads (`tailTurns` / `lastAssistantText` / `summarizeTail`, shared with `session-scan.ts`) and pane classification (`paneIsBusy`). Identity (name / job / memory / lineage) lives in `wizard.ts`, not here. **Busy detection matches the TUI spinner footer** (`… (12m 31s · …)` on current versions, `esc to interrupt` on older ones) in the last 8 non-blank rows — transcript mtime is useless here, it goes quiet during long tool calls.
60
- - `wizard.ts` is the identity layer over those sessions: a target key plus a name (the chat's name; a tagged session reads `chat#tag`), a one-line job description, a long-term `memory` list, and a `parent` link that makes a session a CLONE of another. The registry is a write-through json (`~/.wezard/wizards.json`, `wrc.mirror.wizardsFile`) bound process-wide via `bindWizardStore` so the IM side can read it too; everything else in the module is a pure renderer. **Identity travels as a system prompt, never as a message**: `renderCharter` builds the wizard's charter (who it is, its workspace, its clones, its memory, the tool roster, the orchestration playbook, the etiquette of talking where a human is watching) and `spawn-tmux` presses it in with `--append-system-prompt "$(cat <file>)"` — so it survives `/clear`, costs no turn, and never shows in chat. Every spawn path (`/new`, a dead-pane respawn, a clone) pulls it from `MirrorBridge.setCharterProvider`, installed in `index.ts`. Backends with no such flag (codebuddy) spawn without a charter rather than with a rejected flag.
59
+ - `peers.ts` is the read side of multi-agent awareness — "who else is working in this chat, and are they busy". A wizard's HOME chat is its target key's base principal (`baseOfKey`): the untagged default plus every slot `chat:xxx#slot`. Pure parsing only — bounded transcript-tail reads (`tailTurns` / `lastAssistantText` / `summarizeTail`, shared with `session-scan.ts`) and pane classification (`paneIsBusy`). Identity (name / job / memory / lineage) lives in `wizard.ts`, not here. **Busy detection matches the TUI spinner footer** (`… (12m 31s · …)` on current versions, `esc to interrupt` on older ones) in the last 8 non-blank rows — transcript mtime is useless here, it goes quiet during long tool calls.
60
+ - `wizard.ts` is the identity layer over those sessions: a target key plus a **globally unique name** — the wizard's only address, written `.name` everywhere a human or model sees it (a chat's default session takes the chat's name, a slot takes its slot id; collisions get `-N` via `pickName`; `settleName` claims lazily, `settleAll` migrates at boot, default sessions first). The slot id inside the key is NOT the name: a rename never rekeys a session. Plus a one-line job description, a long-term `memory` list, and a `parent` link that makes a session a CLONE of another. The registry is a write-through json (`~/.wezard/wizards.json`, `wrc.mirror.wizardsFile`) bound process-wide via `bindWizardStore` so the IM side can read it too; everything else in the module is a pure renderer. **Identity travels as a system prompt, never as a message**: `renderCharter` builds the wizard's charter (who it is, its workspace, its clones, its memory, the chat's and the workspace's shared memory (`wizard-memory.ts`: `~/.wezard/memory/{chats,workspaces}/*.md`, human-editable, appended by `wizard_remember({scope})`), the tool roster, the orchestration playbook, the public-vs-private etiquette) and `spawn-tmux` presses it in with `--append-system-prompt "$(cat <file>)"` — so it survives `/clear`, costs no turn, and never shows in chat. Every spawn path (`/new`, a dead-pane respawn, a clone) pulls it from `MirrorBridge.setCharterProvider`, installed in `index.ts`. Backends with no such flag (codebuddy) spawn without a charter rather than with a rejected flag.
61
61
  - **Cloning is a context fork**: `MirrorBridge.cloneSession` spawns `--resume <parent sid> --fork-session`, so the child starts holding everything the parent had read while the parent is untouched — that is what makes "load the shared material once, then fan out" work. Two invariants: `--fork-session` is mandatory (without it a second pane appends to the parent's jsonl and two chats cross-wire), and the forked transcript only appears when that pane takes its FIRST message — so the clone's opening line is the trigger, and the attach waits for the fork; if it never appears the pane is killed rather than bound to the parent's sid.
62
+ - **Chats are public channels, wizard↔wizard is private by default.** `send_peer` injects with `channel:""` and posts no bubble; `public:true` (the model decides) posts `emoji .a → .b` into the sender's current channel and the reply follows there. No spawn/relay bubbles remain — only job open/close and `notify`.
62
63
  - `jobs.ts` is the ledger for one fan-out: which wizards belong to a job, which of them were spawned *for* it (only those get recycled on `close_job` — a long-lived wizard pulled in to help is not), and the two bubbles the chat sees. It holds no control flow on purpose: that stays in the initiating wizard's context, so a reload costs one retry instead of a resurrected state machine. Closed rows older than 24h are dropped on the next write.
63
64
  - `notices.ts` is the other half of awareness. A charter's roster is a spawn-time snapshot and only gets staler; a variation (a birth, a `stop_wizard`, a changed job description, a chat getting named) posts one line into each affected wizard's in-memory box, and the next text injected into that pane — from a human, a peer or a scheduled task — carries it along as a `<system-reminder>`. No extra turn, no bubble, no transcript. Delivery clears the box: a notice is a hint, the truth is always `wizard_roster`.
64
- - `chat-name.ts` turns a chat's unreadable `chat:wr…` id into a writable address, and auto-fills one from the workspace basename when nobody named it. The fill happens wherever a name is about to be handed to a model — including `renderCharter`, since a system prompt outlives the process that read it. Fill-only; a human-chosen name is never overwritten.
65
- - `tasks.ts` fires a scheduled prompt into a target wizard's session — exactly equivalent to someone saying it in the chat at that moment. Lives in the daemon, not in a session, so a CLI restart / `/clear` / session end can't carry it away; the spec is persisted to `config.jsonc`'s `schedules`.
65
+ - `chat-name.ts` turns a chat's unreadable `chat:wr…` id into a readable name (which its default wizard inherits), and auto-fills one from the workspace basename when nobody named it. The fill happens wherever a name is about to be handed to a model — including `renderCharter`, since a system prompt outlives the process that read it. Fill-only; a human-chosen name is never overwritten.
66
+ - `tasks.ts` fires a scheduled prompt into a target wizard's session — exactly equivalent to someone saying it in the chat at that moment. Lives in the daemon, not in a session, so a CLI restart / `/clear` / session end can't carry it away. It runs one fixed skeleton per shot — `trigger → gate → render → dispatch` — and nothing else: the *specifics* of each stage live in the task's own file, because a schedule is "run this logic at this time" and logic does not fit in json.
67
+ - A task is `~/.wezard/tasks/<id>.task.mjs`, an ES module exporting `when` / `gate` / `prompt` / `target`+`fresh` / `owner` (the wizard that scheduled it — its rolepage shows the task under 日程) — **the config is source the wizard edits directly** (hot-reloaded by mtime; a file that fails to load keeps its previous version running and surfaces the reason in `list_tasks`). Source stays source: `lastFired` and the gate's cross-round memo live in `~/.wezard/task-state.json`, so the daemon never writes back over a file a wizard just edited.
68
+ - `shared/trigger.ts` is the trigger AST (Specification + Interpreter). **Clock vs Guard is the whole point**: `every`/`daily`/`at` produce instants and answer "when next", `between`/`onDays`/`not` only veto. `and(clock, …guards)` is their intersection — that's what makes "白天每小时" expressible, and why `nextFire` enumerates clock candidates and picks the first one every guard passes. An AST, not a closure, because it must be renderable (`每 1 小时 · 08:00-21:00`), storable and drawable. `shared/schedule-spec.ts` under it is pure lexer (weekday / time-of-day / interval / window) — it grows with how people speak, the AST grows with how things are computed.
69
+ - `gate` (`task-gate.ts`) runs **in a child process with a timeout**: it is user/wizard-written code that shells out, and a wedged gate must not stall every other schedule — timeout, throw and crash are all the same verdict, *hold fire this round*. Returning `false` costs nothing and says nothing; that is how "only if there's work" stops spending a pane per empty round. A gate **error** posts one bubble (only on the good→bad transition) — a broken gate's silence is indistinguishable from "nothing to do today".
70
+ - **rolepage** (`shared/role-view.ts` pure view model, `shared/role-render.ts`, served by `shared/chat-http.ts` as `/role?id=…&role=…`, legacy `/chat` = same page; frontend `web/chat.js`): one role's IM — every group chat and single chat (human or wizard DM) it took part in. Each turn folds into two messages (in: speaker/peer/task → wizard; out: wizard → them); own messages right, others left, clicking the blank opposite a bubble switches role. Any valid `?id=` ticket may view every role (single-user deployment); the standalone svr has no registry and names roles from the pushed `WorldFacts`.
66
71
  - `graph.ts` is the loop-graph runner: `nodes` = tagged sessions (own cli/model/cwd), `steps` = an ordered pipeline walked `rounds` times, each step injecting a prompt, waiting for that agent to go idle, then feeding its reply forward as `{{last}}` / `{{<tag>}}`. Stops early on the `until` sentinel. All effects go through injected deps backed by `mirror-bridge`, so a step is exactly what a human typing `#tag …` would do. Runs are **in-memory only** — a reload cancels them (panes survive).
67
72
 
68
73
  **Configuration** (`shared/config.ts`):
@@ -75,13 +80,13 @@ Three processes, all coordinating via 127.0.0.1:17890 and `~/.wezard/`:
75
80
  - `shared/json-map-store.ts` (`loadJsonMap`) is the write-through keyed-JSON file behind `sessions.ts`, `mirror-store.ts`, `wizard.ts` and `jobs.ts`. Tolerant at both ends by design: a corrupt file reads as empty, a failed write is swallowed — everything it holds is recovery state, so losing it must cost a re-attach, never a live session. `gc` runs just before each write (jobs expires closed rows that way).
76
81
  - `shared/exec-path.ts` (`augmentedPath`) — PATH widening for every daemon-side spawn.
77
82
  - `shared/std.ts` — `sleep`, `truncate`, `truncateWithCount` (`…(+N)`), `clipLine` (collapse whitespace, then cut).
78
- - `shared/paths.ts` (`expandHome`, `sanitizeId`), `shared/session-label.ts` (`tagOfKey` / `baseOfKey` / `withTagHeader` — use these names, don't re-alias them locally).
83
+ - `shared/paths.ts` (`expandHome`, `sanitizeId`), `shared/session-label.ts` (`tagOfKey` / `baseOfKey` / `withTagHeader` / `nameTokenRe` / `displayName` — use these names, don't re-alias them locally; names reach it through the injected `TagLinker`, since `shared/` can't import the daemon registry).
79
84
 
80
85
  **Hook + sync**:
81
86
  - `hooks/hooks.json` is registered automatically by Claude Code when the plugin is installed (uses `${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}` so the same file works under CodeBuddy's plugin loader if ever published there).
82
87
  - For non-plugin installs (`claude-internal` wrapper), `cli/sync.ts` writes the MCP server entry + `WEZARD_DAEMON_BASE` env into target `settings.json`s listed in `config.sync.targets`. **It deliberately strips legacy hook entries** for `kind:"claude"` targets — registering both the plugin's `hooks.json` AND a settings.json hook fires the hook twice per tool call (= duplicate cards). For `kind:"codebuddy"` targets, since the wezard plugin is not published to CodeBuddy's marketplace, sync **writes the hook directly into `~/.codebuddy/settings.json`** with an absolute path to `hooks/pre-tool-use.sh` — this is the only registration path for CodeBuddy. `~/.wezard/sync.lock.json` tracks what was written so `--remove` is reversible.
83
88
 
84
- **MCP server** (`mcp/server.ts`): stdio transport, completely stateless w.r.t. WeCom. Every tool just `POST`s to the daemon. Add a tool here AND a route handler in `daemon/`. The collaboration tools (`wizard_whoami` / `wizard_identity` / `wizard_roster` / `spawn_clone` / `stop_wizard` / `wizard_remember` / `wizard_handoff_self`, plus the older `list_peers` / `peek_peer` / `send_peer` / `wait_peer` / `run_agent_graph` / `graph_status` / `stop_graph`) all send `selfRef()` = `{sessionId, tmuxPane}` so the daemon can resolve *which* session is asking — env sessionId goes stale after a `/clear`, pane id doesn't. Their descriptions are the whole discovery mechanism for wizard↔wizard collaboration: a wizard only realizes it can clone itself, or drive `#fix`, because the tool descriptions say so. Tool NAMES stayed on the older `peer` vocabulary on purpose — a name is an address, and renaming would break every hint, doc and habit pointing at it; the descriptions carry the wizard/clone vocabulary.
89
+ **MCP server** (`mcp/server.ts`): stdio transport, completely stateless w.r.t. WeCom. Every tool just `POST`s to the daemon. Add a tool here AND a route handler in `daemon/`. The collaboration tools (`wizard_whoami` / `wizard_identity` / `wizard_roster` / `spawn_clone` / `stop_wizard` / `wizard_remember` / `wizard_handoff_self`, plus the older `list_peers` / `peek_peer` / `send_peer` / `wait_peer` / `run_agent_graph` / `graph_status` / `stop_graph`) all send `selfRef()` = `{sessionId, tmuxPane}` so the daemon can resolve *which* session is asking — env sessionId goes stale after a `/clear`, pane id doesn't. Their descriptions are the whole discovery mechanism for wizard↔wizard collaboration: a wizard only realizes it can clone itself, or drive `.fix`, because the tool descriptions say so. Their address parameter is `name` (`fix` / `.fix`); the daemon still accepts the legacy `tag`/`tags` fields, because an already-running wizard's MCP process is old code. Tool NAMES stayed on the older `peer` vocabulary on purpose — a name is an address, and renaming would break every hint, doc and habit pointing at it; the descriptions carry the wizard/clone vocabulary.
85
90
 
86
91
  ## Important patterns / pitfalls
87
92