tandem-editor 0.21.0 → 0.22.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tandem",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Edit and iterate on documents with any MCP-capable AI. Claude is the default and best-supported client today.",
5
5
  "author": {
6
6
  "name": "Tandem"
@@ -11,14 +11,14 @@
11
11
  "mcpServers": {
12
12
  "tandem": {
13
13
  "command": "npx",
14
- "args": ["-y", "tandem-editor@0.21.0", "mcp-stdio"],
14
+ "args": ["-y", "tandem-editor@0.22.0", "mcp-stdio"],
15
15
  "env": {
16
16
  "TANDEM_URL": "http://127.0.0.1:3479"
17
17
  }
18
18
  },
19
19
  "tandem-channel": {
20
20
  "command": "npx",
21
- "args": ["-y", "tandem-editor@0.21.0", "channel"],
21
+ "args": ["-y", "tandem-editor@0.22.0", "channel"],
22
22
  "env": {
23
23
  "TANDEM_URL": "http://127.0.0.1:3479"
24
24
  }
@@ -28,13 +28,13 @@
28
28
  "monitors": [
29
29
  {
30
30
  "name": "tandem-events",
31
- "command": "npx -y tandem-editor@0.21.0 monitor",
31
+ "command": "npx -y tandem-editor@0.22.0 monitor",
32
32
  "description": "Tandem real-time document events (annotations, chat, selections)",
33
33
  "when": "on-skill-invoke:tandem:tandem"
34
34
  },
35
35
  {
36
36
  "name": "tandem-events-user-skill",
37
- "command": "npx -y tandem-editor@0.21.0 monitor",
37
+ "command": "npx -y tandem-editor@0.22.0 monitor",
38
38
  "description": "Tandem real-time document events (annotations, chat, selections)",
39
39
  "when": "on-skill-invoke:tandem"
40
40
  }
package/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.22.0] - 2026-08-11
11
+
12
+ This release is about your AI noticing that you said something.
13
+
14
+ Tandem has always been able to tell your AI that you commented on a paragraph or sent a chat message, but for sessions you started yourself, something had to be listening first — and nothing arranged that for you. A session you opened by typing `claude` would sit there while your comments piled up unread. Now it sets that up itself, on its own, the first time it touches Tandem, and Tandem says how at the moment the connection opens rather than hoping the subject comes up later. Measured across ten isolated sessions, the number that arranged it went from three in six to six in six.
15
+
16
+ The rest is a smaller thing that will save you a round trip: bug reports now arrive with the details we would otherwise have to ask you for, and the Report a bug link fills them in.
17
+
18
+ ### Changed
19
+
20
+ - **Hand-started Claude Code sessions now try to watch Tandem automatically on first use.** After the first successful read-only `tandem_status`, the bundled skill makes one persistent built-in Monitor attempt using the server-reported wake address; asking Claude to watch is now recovery rather than setup. Existing setup-managed skill copies refresh before Tandem reports ready, including when the launcher is disabled or deferred. A missing standalone skill does not install during generic startup, so plugin-only users do not silently acquire a second integration route. Plugin-only sessions receive the updated instructions when Claude Code refreshes its plugin cache, on a timeline Tandem does not control. If a plugin-backed session starts both its packaged monitor and the built-in watch automatically, wakes remain payload-free and inbox reads de-duplicate; `TaskStop` stops the built-in watch when doubled wakes make the overlap visible.
21
+
22
+ - **Tandem now tells your AI how to be woken the moment the connection opens, not only once it reaches for the Tandem skill (#1397).** Tandem's connection has always been able to carry a short standing instruction, and Tandem sent nothing. That text is the only thing besides tool *names* that reaches your AI before it calls anything, so guidance about staying reachable arrived — when it arrived at all — after the moment a session decides how to behave. It now says that the inbox is the authority on your activity, that a client able to hold a persistent watch should open one on the address Tandem reports, and that annotations are held in solo mode. Sessions Tandem launches for you are told to skip the watch, because they are already woken directly and a second watch would deliver everything twice. It deliberately makes no claim about whether *this* session is covered and quotes no subscriber count — Tandem cannot see either, and a standing instruction is not a diagnosis. AI clients other than Claude Code receive the same text; nothing in it names a capability as a requirement, so it reads as inapplicable rather than wrong where no such watch exists. Measured across ten isolated authenticated sessions, first-use arming went from three of six to six of six with this and the two skill changes above in place — the three together, not this one alone.
23
+
24
+ - **Copy Diagnostics now carries the facts a bug report needs, and the Report-a-bug link fills them in for you (#1394).** Diagnostics used to describe Tandem and almost nothing about the machine it ran on, so most reports opened with a round trip asking for the operating system version, the browser, and how much memory the machine had. Both the copied text and the `tandem_diagnostics` tool now include platform, architecture, OS release and version, CPU model and core count, total memory, Node version, and — in the browser distribution — a short browser descriptor. The Report a bug link in Settings opens a GitHub issue with that block already in the body instead of an empty form. **What is deliberately absent stays absent**, because this text lands in a public issue: your machine name, your username, your home directory path, your network interfaces, your locale and timezone. Paths that do survive are collapsed — your home directory becomes `~`, Tandem's data directory becomes `<app-data>` — including paths that arrive inside raw filesystem error messages, which no prefix list can be trusted to cover. The link gathers diagnostics only after your pointer or keyboard focus rests on it briefly, and not merely passes through: collecting them is not an inert read — it opens and drops a real connection to Tandem's own update stream and runs a global package listing — so a pointer crossing the sidebar on its way elsewhere must not trigger it. If gathering fails, the link still works, unfilled.
25
+
26
+ ### Tests
27
+
28
+ - **A live-evidence gate for first-use session-monitor arming (#1393).** That a hand-started session arms a watch on first use could previously only be argued from source. This adds a harness that builds an isolated Claude Code and Tandem fixture — its own home, config directory and authentication — and drives real bounded sessions across a ten-trial matrix, emitting signed pass/fail evidence per trial. Two properties are load-bearing: a recorded decline must be shown to be the model's own choice rather than a broken rig, and a re-attempt supersedes prior evidence without erasing it. The suite is not wired into CI; #1399 records why and what it would cost. Harness only — no product code.
29
+
10
30
  ## [0.21.0] - 2026-08-10
11
31
 
12
32
  Most of this release is Tandem telling you the truth about itself.
package/README.md CHANGED
@@ -107,9 +107,9 @@ You do not have to configure any of this by hand. Tandem opens a setup wizard th
107
107
 
108
108
  Prefer a different AI? Any MCP-capable client can connect to the same endpoint — see the [compatibility table](#the-mcp-integration-policy) for what is supported and what is untested, and [Cowork](#cowork) for connecting Claude Desktop on Windows.
109
109
 
110
- **How Claude starts matters.** Tandem talks to Claude over two independent connections: one lets Claude read and edit your document, the other tells it the moment you comment or send a chat message. Sessions Tandem launches for you — including the desktop app's **Relaunch Claude** button — get both, and need no setup from you. A session you start yourself by typing `claude` gets only the first until you add one of three things.
110
+ **How Claude starts matters.** Tandem talks to Claude over two independent connections: one lets Claude read and edit your document, the other tells it the moment you comment or send a chat message. Sessions Tandem launches for you — including the desktop app's **Relaunch Claude** button — get both, and need no setup from you. A session you start yourself by typing `claude` begins with the first connection; on its first successful read-mode `tandem_status`, Tandem's bundled skill automatically tells it to attempt the second with the built-in Monitor when that tool is available.
111
111
 
112
- The simplest needs no installation at all: **ask Claude to watch for updates.** Tandem's bundled skill tells it how, so "watch Tandem for updates while we work" is usually enough it opens a watch on Tandem's wake stream and is woken whenever you comment or send a message. Nothing to install, no flag, and it lasts as long as that session. It does need a Claude Code that offers a `Monitor` tool. That is enabled per account rather than per version, so upgrading will not add it, and on Windows it also needs Git Bash. If Claude tells you it has none, skip to the third option below the plugin needs the same tool, so it will not help either.
112
+ The simplest needs no installation at all: **the built-in Monitor watch starts automatically on first Tandem use.** The bundled skill reads the live wake-stream address from that first successful `tandem_status` and makes one persistent attempt for the session. If that attempt was skipped, asking Claude to watch is a recovery step, not normal setup. It does need a Claude Code that offers a built-in Monitor tool. That is enabled per account rather than per version, so upgrading will not add it, and on Windows it also needs Git Bash. The plugin monitor shares the same per-account feature gate, so it cannot help when that gate is off. But the plugin monitor does not require Git Bash on Windows and can fall back to PowerShell, so it may help when Git Bash is the missing precondition. The third option below, the channel shim, avoids both requirements.
113
113
 
114
114
  The second is to install the Tandem plugin, which also needs no flag — see [Real-time updates](#real-time-updates) below. It starts watching the first time Claude uses Tandem's skill in a session, so ask for Tandem by name ("let's work on this in Tandem") rather than expecting it to be listening before you have mentioned it. Start `claude` from a terminal window if you install it: the plugin's monitor uses whatever program path that shell has, and a Claude Code started from a desktop icon may not have one it can use.
115
115
 
@@ -120,7 +120,7 @@ tandem setup --apply --with-channel-shim # once
120
120
  claude --dangerously-load-development-channels server:tandem-channel # every session
121
121
  ```
122
122
 
123
- Use one of the three, not several: each delivers independently, so a session running two receives every event twice.
123
+ Choose one setup route where possible. A plugin-backed session whose Claude Code also offers the built-in Monitor may start both automatically when it first uses Tandem's skill. Those wakes contain no message content; host rate limiting can hide the overlap, and the inbox de-duplicates what Claude reads. If wakes visibly double, ask Claude to stop its built-in watch with `TaskStop`.
124
124
 
125
125
  Without any of them nothing breaks and nothing is lost: your messages and comments are saved, and Claude sees them the next time it checks its inbox. But it will not react on its own, which reads as Claude ignoring you. If that is what you are seeing, [this troubleshooting entry](docs/troubleshooting.md#i-sent-a-chat-message-or-left-a-comment-and-nothing-happened) walks through it, and **Settings → About → Copy Diagnostics** (or `tandem doctor`, on npm installs) reports whether anything is listening. More detail in [Real-time updates](#real-time-updates).
126
126
 
@@ -255,11 +255,11 @@ Tandem's MCP tools span six capability areas. Full reference: [docs/mcp-tools.md
255
255
 
256
256
  ### Real-time updates
257
257
 
258
- Real-time delivery gets events (annotation actions, chat messages) to the AI the moment they happen, so it does not have to wait until it next polls. There are three ways to get it, and they are alternativesnot layers.
258
+ Real-time delivery gets events (annotation actions, chat messages) to the AI the moment they happen, so it does not have to wait until it next polls. There are three setup routes; they are not layers you need to configure together. When the plugin and built-in Monitor are both available, however, first skill use can start both automatically a safe but potentially wasteful overlap described below.
259
259
 
260
260
  None of them is needed for a session Tandem launches for you. Those are woken directly by Tandem over the session's own input and use none of the three; everything below is about sessions you start by hand.
261
261
 
262
- **A self-armed watch** needs nothing installed and no flag. Ask Claude to watch Tandem for updates and it opens a watch on Tandem's wake stream itself; the bundled skill carries the instructions, and `tandem_status` reports the address to watch. It lasts for that session and disappears with it, which is also the point — nothing is left configured on your machine. This is the path that survives the failures the other two are prone to, so it is the one to try first. Two limits: it needs a Claude Code that offers a `Monitor` tool, and the wake stream is **loopback-only**, so it does not reach a session running anywhere but this machine — Cowork included.
262
+ **A self-armed watch** needs nothing installed and no flag. On first Tandem use, the bundled skill automatically reads the wake-stream address from the first successful `tandem_status` and makes one persistent built-in Monitor attempt. It lasts for that session and disappears with it, which is also the point — nothing is left configured on your machine. Asking Claude to watch is recovery if the automatic attempt was skipped. Two limits: it needs a Claude Code that offers a built-in Monitor tool, and the wake stream is **loopback-only**, so it does not reach a session running anywhere but this machine — Cowork included.
263
263
 
264
264
  **The plugin monitor** needs no flag either, and once installed it applies to every session — but it starts watching only when Claude first uses Tandem's skill in that session, not the moment the session opens. In practice that means asking for Tandem by name; a Claude that has never been told about Tandem is not listening yet. That is deliberate: it used to start in every session including ones with nothing to do with Tandem, so when it could not run it reported a failure in all of them. Two further conditions: it requires **Claude Code 2.1.212 or newer** — on older versions the install still succeeds and the monitor simply never runs, with nothing to tell you so — and you should start `claude` from a terminal, because the monitor runs with whatever program path that session was given and a desktop-icon launch may not include Node. If it reports `exit 127`, that is this.
265
265
 
@@ -272,7 +272,7 @@ claude --dangerously-load-development-channels server:tandem-channel # every s
272
272
 
273
273
  The flag is Claude Code's marker for unstable APIs. It works only in an interactive session, and it is required rather than optional: `tandem-channel` is a plain MCP server, and Claude Code's channel allowlist covers only plugins, so there is no list Tandem could join that would remove the need for it. If you already have it configured, your configuration is untouched and keeps working — the change is to what a *new* setup writes. To remove it, re-run `tandem setup --apply` without the flag.
274
274
 
275
- Do not enable more than one: each delivers independently, so a session running two receives every event twice.
275
+ Choose one setup route where possible. A plugin-backed session may still start its packaged monitor and built-in watch automatically. Wakes carry no message content, the inbox de-duplicates what Claude reads, and host rate limiting can hide the overlap; if wakes visibly double, ask Claude to stop its built-in watch with `TaskStop`.
276
276
 
277
277
  Note that Solo mode suppresses annotation events on all three by design — chat still comes through. If you are testing real-time delivery and only chat arrives, check the mode toggle before debugging the transport.
278
278
 
package/dist/cli/index.js CHANGED
@@ -101,19 +101,27 @@ var init_platform = __esm({
101
101
  import { execFile } from "child_process";
102
102
  import { join } from "path";
103
103
  import { promisify } from "util";
104
+ function throwIfAborted(signal) {
105
+ if (signal?.aborted) {
106
+ throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
107
+ }
108
+ }
104
109
  function systemBin(name) {
105
110
  return join(process.env.SystemRoot ?? "C:\\Windows", "System32", name);
106
111
  }
107
- async function runPowerShell(script, env) {
112
+ async function runPowerShell(script, env, signal) {
108
113
  const args2 = ["-NoProfile", "-NonInteractive", "-Command", script];
114
+ throwIfAborted(signal);
109
115
  try {
110
- return await execFileAsync("pwsh.exe", args2, { env });
116
+ return await execFileAsync("pwsh.exe", args2, { env, signal });
111
117
  } catch (err) {
118
+ if (signal?.aborted) throw signal.reason ?? err;
112
119
  const code = err.code;
113
120
  if (code !== "ENOENT") throw err;
114
121
  try {
115
- return await execFileAsync("powershell.exe", args2, { env });
122
+ return await execFileAsync("powershell.exe", args2, { env, signal });
116
123
  } catch (fallbackErr) {
124
+ if (signal?.aborted) throw signal.reason ?? fallbackErr;
117
125
  throw new Error(
118
126
  `runPowerShell: both pwsh.exe and powershell.exe failed (pwsh: ${err.message})`,
119
127
  { cause: fallbackErr }
@@ -121,9 +129,13 @@ async function runPowerShell(script, env) {
121
129
  }
122
130
  }
123
131
  }
124
- async function getCurrentUserSid() {
132
+ async function getCurrentUserSid(signal) {
125
133
  if (cachedCurrentUserSid !== null) return cachedCurrentUserSid;
126
- const { stdout } = await execFileAsync(systemBin("whoami.exe"), ["/user", "/fo", "csv", "/nh"]);
134
+ throwIfAborted(signal);
135
+ const { stdout } = await execFileAsync(systemBin("whoami.exe"), ["/user", "/fo", "csv", "/nh"], {
136
+ signal
137
+ });
138
+ throwIfAborted(signal);
127
139
  const match = stdout.match(/"(S-[\d-]+)"\s*$/m);
128
140
  if (!match) {
129
141
  throw new Error(`getCurrentUserSid: could not parse SID from whoami output: ${stdout.trim()}`);
@@ -133,29 +145,39 @@ async function getCurrentUserSid() {
133
145
  }
134
146
  async function setRestrictiveAcl(path9, opts = {}) {
135
147
  if (process.platform !== "win32") return;
136
- const sid = await getCurrentUserSid();
148
+ const { signal } = opts;
149
+ throwIfAborted(signal);
150
+ const sid = await getCurrentUserSid(signal);
137
151
  const rights = opts.inheritable ? "(OI)(CI)F" : "F";
138
152
  try {
139
- await execFileAsync(systemBin("icacls.exe"), [
140
- path9,
141
- "/inheritance:r",
142
- "/grant:r",
143
- `*${sid}:${rights}`
144
- ]);
153
+ await execFileAsync(
154
+ systemBin("icacls.exe"),
155
+ [path9, "/inheritance:r", "/grant:r", `*${sid}:${rights}`],
156
+ { signal }
157
+ );
145
158
  } catch (err) {
159
+ if (signal?.aborted) throw signal.reason ?? err;
146
160
  throw new Error(`setRestrictiveAcl: icacls failed on ${path9}: ${err.message}`, {
147
161
  cause: err
148
162
  });
149
163
  }
150
- await assertNoBroadAce(path9);
164
+ throwIfAborted(signal);
165
+ await assertNoBroadAce(path9, { signal });
151
166
  }
152
- async function assertNoBroadAce(path9) {
167
+ async function assertNoBroadAce(path9, opts = {}) {
153
168
  if (process.platform !== "win32") return;
169
+ const { signal } = opts;
170
+ throwIfAborted(signal);
154
171
  const script = "Import-Module Microsoft.PowerShell.Security; (Get-Acl -LiteralPath $env:TANDEM_ACL_PATH).Sddl";
155
- const { stdout } = await runPowerShell(script, {
156
- ...process.env,
157
- TANDEM_ACL_PATH: path9
158
- });
172
+ const { stdout } = await runPowerShell(
173
+ script,
174
+ {
175
+ ...process.env,
176
+ TANDEM_ACL_PATH: path9
177
+ },
178
+ signal
179
+ );
180
+ throwIfAborted(signal);
159
181
  const sddl = stdout.trim();
160
182
  for (const fragment of BROAD_SDDL_FRAGMENTS) {
161
183
  if (sddl.includes(fragment)) {
@@ -477,7 +499,7 @@ function resolveChannelDist(env = process.env, exists = existsSync) {
477
499
  return resolve3(PACKAGE_ROOT, "dist/channel/index.js");
478
500
  }
479
501
  function resolveCliVersion() {
480
- if (true) return "0.21.0";
502
+ if (true) return "0.22.0";
481
503
  if (typeof __APP_VERSION__ !== "undefined") return __APP_VERSION__;
482
504
  for (const rel of ["../../package.json", "../../../package.json"]) {
483
505
  try {
@@ -634,19 +656,42 @@ function detectTargets(opts = {}) {
634
656
  }
635
657
  return targets;
636
658
  }
637
- async function atomicWrite(content, dest) {
659
+ function throwIfAborted2(signal) {
660
+ if (signal?.aborted) {
661
+ throw signal.reason ?? new DOMException("The operation was aborted", "AbortError");
662
+ }
663
+ }
664
+ async function atomicWrite(content, dest, opts = {}) {
665
+ const { signal } = opts;
666
+ throwIfAborted2(signal);
638
667
  const tmp = join4(dirname2(dest), `.tandem-setup-${randomUUID2()}.tmp`);
639
- await writeFile(tmp, content, { encoding: "utf-8", mode: 384 });
668
+ try {
669
+ await writeFile(tmp, content, { encoding: "utf-8", mode: 384, signal });
670
+ } catch (writeErr) {
671
+ await unlinkOrLeak(tmp, writeErr);
672
+ throw writeErr;
673
+ }
640
674
  try {
641
675
  if (process.platform === "win32") {
642
- await setRestrictiveAcl(tmp);
676
+ await setRestrictiveAcl(tmp, { signal });
643
677
  } else {
678
+ throwIfAborted2(signal);
644
679
  await chmod(tmp, 384);
645
680
  }
681
+ throwIfAborted2(signal);
646
682
  } catch (tightenErr) {
647
683
  await unlinkOrLeak(tmp, tightenErr);
648
684
  throw tightenErr;
649
685
  }
686
+ try {
687
+ if (process.env.VITEST === "true") await opts.beforeCommitForTests?.();
688
+ throwIfAborted2(signal);
689
+ if (opts.requireExistingDest) await stat(dest);
690
+ throwIfAborted2(signal);
691
+ } catch (commitGuardErr) {
692
+ await unlinkOrLeak(tmp, commitGuardErr);
693
+ throw commitGuardErr;
694
+ }
650
695
  try {
651
696
  await rename(tmp, dest);
652
697
  } catch (err) {
@@ -665,6 +710,7 @@ async function unlinkOrLeak(path9, originalErr) {
665
710
  try {
666
711
  await unlink(path9);
667
712
  } catch (cleanupErr) {
713
+ if (cleanupErr.code === "ENOENT") return;
668
714
  if (originalErr instanceof Error && originalErr.cause === void 0) {
669
715
  originalErr.cause = cleanupErr;
670
716
  }
@@ -3531,21 +3577,21 @@ function evaluatePushPath(push) {
3531
3577
  return {
3532
3578
  status: "warn",
3533
3579
  message: "No real-time push consumer attached \u2014 Claude is not notified when you comment or send a chat message, and only sees them when it polls its inbox",
3534
- fix: "This affects sessions you start yourself; sessions Tandem launches are woken directly and do not use this path. Simplest first: ask Claude to watch for updates \u2014 it can arm a watch on Tandem's wake stream itself, with nothing to install and no flag (the bundled skill tells it how; `tandem_status` reports the address). That one needs a Monitor tool, which not every Claude Code has \u2014 it is enabled per account rather than per version, so upgrading will not add it, and on Windows it also needs Git Bash. If Claude says it has none, go straight to the channel shim below: the plugin monitor needs the same tool and will not help. Otherwise install the Tandem plugin, which registers a monitor needing no flag (`claude plugin list` to check) \u2014 start `claude` from a terminal if you do, since the monitor inherits that shell's PATH and cannot find Node without it \u2014 or register the channel shim with `tandem setup --apply --with-channel-shim` and start Claude Code with `--dangerously-load-development-channels server:tandem-channel`. Use ONE of the three, not several \u2014 each delivers independently, so running two means every event arrives twice.",
3580
+ fix: "This affects sessions you start yourself; sessions Tandem launches are woken directly and do not use this path. Simplest first: ask Claude to watch for updates \u2014 it can arm a watch on Tandem's wake stream itself, with nothing to install and no flag (the bundled skill tells it how; `tandem_status` reports the address). That one needs a Monitor tool, which not every Claude Code has \u2014 it is enabled per account rather than per version, so upgrading will not add it, and on Windows it also needs Git Bash. If Claude says it has none, the channel shim below is the option that always avoids Monitor. The plugin monitor shares that per-account feature gate, so it will not help if the gate is off; however, the plugin monitor does not require Git Bash on Windows and can fall back to PowerShell, so it can help when Git Bash is the missing precondition. You can install the Tandem plugin, which registers a monitor needing no flag (`claude plugin list` to check) \u2014 start `claude` from a terminal if you do, since the monitor inherits that shell's PATH and cannot find Node without it \u2014 or register the channel shim with `tandem setup --apply --with-channel-shim` and start Claude Code with `--dangerously-load-development-channels server:tandem-channel`. Choose one setup route where possible. When the plugin and built-in Monitor are both available, invoking Tandem's skill can start both automatically; doubled wakes carry no content and the inbox de-duplicates, but they can waste a turn. Ask Claude to stop its built-in watch with `TaskStop` if that happens.",
3535
3581
  data
3536
3582
  };
3537
3583
  }
3538
3584
  if (eventCount === 0) {
3539
3585
  return {
3540
3586
  status: "pass",
3541
- message: `${subscribers} push consumer(s) attached; no events delivered yet this run (expected if you haven't edited, or if you're in Solo mode)`,
3587
+ message: `${subscribers} push consumer(s) attached; no events delivered yet this run (expected if you haven't edited, or if you're in Solo mode; this is a process-global count, NOT that this Claude session is covered)`,
3542
3588
  data
3543
3589
  };
3544
3590
  }
3545
3591
  const agoS = p.lastEventAt ? Math.round((Date.now() - p.lastEventAt) / 1e3) : null;
3546
3592
  return {
3547
3593
  status: "pass",
3548
- message: `${subscribers} push consumer(s) attached; ${eventCount} event(s) delivered this run, most recently ${agoS === null ? "?" : `${agoS}s`} ago (confirms events reach a consumer, NOT that Claude sees them)`,
3594
+ message: `${subscribers} push consumer(s) attached; ${eventCount} event(s) delivered this run, most recently ${agoS === null ? "?" : `${agoS}s`} ago (confirms events reach a process-global consumer, NOT that this Claude session is covered or saw them)`,
3549
3595
  data
3550
3596
  };
3551
3597
  }
@@ -3755,7 +3801,7 @@ function evaluateStaleGlobal(bundled, globalVersion) {
3755
3801
  };
3756
3802
  }
3757
3803
  async function checkStaleGlobal(r) {
3758
- const bundled = true ? "0.21.0" : null;
3804
+ const bundled = true ? "0.22.0" : null;
3759
3805
  if (!bundled) return;
3760
3806
  let globalVersion;
3761
3807
  try {
@@ -4862,7 +4908,7 @@ process.once("unhandledRejection", (reason) => {
4862
4908
  `);
4863
4909
  process.exit(1);
4864
4910
  });
4865
- var version = true ? "0.21.0" : "0.0.0-dev";
4911
+ var version = true ? "0.22.0" : "0.0.0-dev";
4866
4912
  var args = process.argv.slice(2);
4867
4913
  if (args.includes("--help") || args.includes("-h")) {
4868
4914
  console.log(`tandem v${version}