@selesai/code 0.13.38 → 0.13.40

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 (92) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/core/intercom-rpc.d.ts +135 -0
  3. package/dist/core/intercom-rpc.js +64 -0
  4. package/dist/core/release-rpc.d.ts +137 -0
  5. package/dist/core/release-rpc.js +77 -0
  6. package/dist/core/resource-loader.d.ts +3 -0
  7. package/dist/core/resource-loader.js +3 -0
  8. package/dist/core/subagent-rpc.d.ts +110 -0
  9. package/dist/core/subagent-rpc.js +60 -0
  10. package/dist/extensions/agent-browser.ts +9 -0
  11. package/dist/extensions/pi-graft/cli.test.ts +5 -5
  12. package/dist/extensions/pi-graft/cli.ts +10 -5
  13. package/dist/extensions/pi-graft/commands.test.ts +5 -5
  14. package/dist/extensions/pi-graft/graft.integration.test.ts +1 -1
  15. package/dist/extensions/pi-graft/lifecycle.test.ts +26 -26
  16. package/dist/extensions/pi-graft/prompt.test.ts +1 -1
  17. package/dist/extensions/pi-graft/state.test.ts +9 -9
  18. package/dist/extensions/pi-graft/test-support.ts +1 -1
  19. package/dist/extensions/pi-graft/tools.test.ts +1 -1
  20. package/dist/extensions/pi-intercom/README.md +15 -0
  21. package/dist/extensions/pi-intercom/broker/broker.ts +277 -13
  22. package/dist/extensions/pi-intercom/broker/client.ts +6 -2
  23. package/dist/extensions/pi-intercom/broker/mailbox-persistence.test.ts +523 -0
  24. package/dist/extensions/pi-intercom/broker/paths.test.ts +4 -4
  25. package/dist/extensions/pi-intercom/broker/paths.ts +4 -2
  26. package/dist/extensions/pi-intercom/broker/spawn.test.ts +1 -3
  27. package/dist/extensions/pi-intercom/extension-api.ts +10 -0
  28. package/dist/extensions/pi-intercom/index.ts +188 -8
  29. package/dist/extensions/pi-intercom/intercom.integration.test.ts +280 -0
  30. package/dist/extensions/pi-intercom/package.json +1 -1
  31. package/dist/extensions/pi-intercom/release-readiness.test.ts +116 -0
  32. package/dist/extensions/pi-intercom/release-readiness.ts +93 -0
  33. package/dist/extensions/pi-subagents/docs/extension-api.md +8 -0
  34. package/dist/extensions/pi-subagents/docs/observability.md +8 -0
  35. package/dist/extensions/pi-subagents/src/extension/index.ts +51 -1
  36. package/dist/extensions/pi-subagents/src/extension/observability.ts +164 -0
  37. package/dist/extensions/pi-subagents/src/extension/release-readiness.ts +367 -0
  38. package/dist/extensions/pi-subagents/src/extension/rpc.ts +8 -1
  39. package/dist/extensions/pi-subagents/src/integrations/pi-web-session-liveness.ts +7 -1
  40. package/dist/extensions/pi-subagents/src/missions/workflow-state.ts +1 -44
  41. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +6 -0
  42. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +13 -3
  43. package/dist/extensions/pi-subagents/src/runs/background/result-delivery-ownership.ts +269 -7
  44. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +11 -10
  45. package/dist/extensions/pi-subagents/src/runs/background/retained-nested-route-tracker.ts +15 -3
  46. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +64 -0
  47. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +69 -6
  48. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -1
  49. package/dist/extensions/pi-subagents/src/shared/owner-record.ts +107 -0
  50. package/dist/extensions/pi-subagents/src/shared/process-identity.ts +87 -0
  51. package/dist/extensions/pi-subagents/src/shared/types.ts +9 -0
  52. package/dist/extensions/pi-subagents/test/integration/async-attention-clearing.test.ts +191 -0
  53. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +63 -0
  54. package/dist/extensions/pi-subagents/test/integration/stop-all-background.test.ts +142 -0
  55. package/dist/extensions/pi-subagents/test/support/fake-child-session.ts +8 -0
  56. package/dist/extensions/pi-subagents/test/unit/observability.test.ts +198 -0
  57. package/dist/extensions/pi-subagents/test/unit/process-identity.test.ts +75 -0
  58. package/dist/extensions/pi-subagents/test/unit/release-readiness.test.ts +715 -0
  59. package/dist/extensions/pi-subagents/test/unit/result-takeover.test.ts +332 -0
  60. package/dist/extensions/tokenin-onboarding.ts +142 -3
  61. package/dist/index.d.ts +6 -1
  62. package/dist/index.js +7 -0
  63. package/dist/main.d.ts +6 -0
  64. package/dist/main.js +1 -1
  65. package/dist/modes/index.d.ts +1 -1
  66. package/dist/modes/rpc/intercom-bridge.d.ts +14 -0
  67. package/dist/modes/rpc/intercom-bridge.js +75 -0
  68. package/dist/modes/rpc/intercom-bridge.test.d.ts +1 -0
  69. package/dist/modes/rpc/intercom-bridge.test.js +65 -0
  70. package/dist/modes/rpc/release-bridge.d.ts +35 -0
  71. package/dist/modes/rpc/release-bridge.js +159 -0
  72. package/dist/modes/rpc/release-bridge.test.d.ts +1 -0
  73. package/dist/modes/rpc/release-bridge.test.js +347 -0
  74. package/dist/modes/rpc/rpc-client.d.ts +34 -10
  75. package/dist/modes/rpc/rpc-client.js +45 -2
  76. package/dist/modes/rpc/rpc-intercom-transport.test.d.ts +1 -0
  77. package/dist/modes/rpc/rpc-intercom-transport.test.js +85 -0
  78. package/dist/modes/rpc/rpc-mode.d.ts +1 -13
  79. package/dist/modes/rpc/rpc-mode.js +239 -8
  80. package/dist/modes/rpc/rpc-release-transport.test.d.ts +1 -0
  81. package/dist/modes/rpc/rpc-release-transport.test.js +329 -0
  82. package/dist/modes/rpc/rpc-subagent-transport.test.d.ts +1 -0
  83. package/dist/modes/rpc/rpc-subagent-transport.test.js +60 -0
  84. package/dist/modes/rpc/rpc-types.d.ts +12 -2
  85. package/dist/modes/rpc/rpc-types.js +0 -6
  86. package/dist/modes/rpc/subagent-bridge.d.ts +23 -0
  87. package/dist/modes/rpc/subagent-bridge.js +93 -0
  88. package/dist/modes/rpc/test-fixtures/intercom-host.d.ts +1 -0
  89. package/dist/modes/rpc/test-fixtures/intercom-host.js +13 -0
  90. package/docs/rpc-subagent.schema.json +491 -0
  91. package/docs/rpc.md +210 -1
  92. package/package.json +2 -1
@@ -68,8 +68,8 @@ describe("installGraft", () => {
68
68
  it("runs the compatible global npm install without a shell off Windows", async () => {
69
69
  const { pi } = makePi(async () => execReturning(OK));
70
70
  const run = await installGraft(pi.exec as never, "/repo");
71
- expect(GRAFT_INSTALL_SPEC).toBe("@nanonets/graft@^0.18");
72
- expect(INSTALL_COMMAND).toBe("npm install -g @nanonets/graft@^0.18");
71
+ expect(GRAFT_INSTALL_SPEC).toBe("@nanonets/graft@^0.21");
72
+ expect(INSTALL_COMMAND).toBe("npm install -g @nanonets/graft@^0.21");
73
73
  const [command, args, options] = pi.exec.mock.calls[0] as [string, string[], { cwd: string; timeout: number }];
74
74
  expect(command).toBe(process.platform === "win32" ? "cmd" : "npm");
75
75
  expect(args).toContain("install");
@@ -118,7 +118,7 @@ describe("version handling", () => {
118
118
 
119
119
  it("accepts the verified range and rejects the boundaries", () => {
120
120
  expect(isCompatibleVersion(MIN_GRAFT_VERSION)).toBe(true);
121
- expect(isCompatibleVersion("0.18.4")).toBe(true);
121
+ expect(isCompatibleVersion("0.21.1")).toBe(true);
122
122
  expect(isCompatibleVersion("0.99.0")).toBe(true);
123
123
  expect(isCompatibleVersion("0.17.9")).toBe(false);
124
124
  expect(isCompatibleVersion(MAX_GRAFT_VERSION_EXCLUSIVE)).toBe(false);
@@ -128,9 +128,9 @@ describe("version handling", () => {
128
128
 
129
129
  describe("probeGraft", () => {
130
130
  it("accepts a supported version and reports it", async () => {
131
- const { pi } = makePi(async () => execReturning({ ...OK, stdout: "0.18.0\n" }));
131
+ const { pi } = makePi(async () => execReturning({ ...OK, stdout: "0.21.1\n" }));
132
132
  const probe = await probeGraft(pi.exec as never, "/repo");
133
- expect(probe).toMatchObject({ kind: "ok", version: "0.18.0" });
133
+ expect(probe).toMatchObject({ kind: "ok", version: "0.21.1" });
134
134
  if (probe.kind === "ok") expect(probe.argv.at(-1)).toBe("--version");
135
135
  });
136
136
 
@@ -15,7 +15,7 @@
15
15
  * Selesai model for deep enrichment, otherwise stay structural and local.
16
16
  * - Extension-spawned commands default to telemetry opt-out (`DO_NOT_TRACK=1`).
17
17
  *
18
- * Verified against @nanonets/graft 0.18.0 (see {@link MIN_GRAFT_VERSION}).
18
+ * Verified against @nanonets/graft 0.21.1 (see {@link MIN_GRAFT_VERSION}).
19
19
  */
20
20
 
21
21
  import { truncateHead, type ExecOptions, type ExecResult } from "@selesai/code";
@@ -27,8 +27,13 @@ export const GRAFT_PACKAGE = "@nanonets/graft";
27
27
  * Lowest Graft CLI whose documented surface this adapter was verified against:
28
28
  * `ask --source/--json`, `skeleton`, `callers --direction/-d`, `grep --in/-i/--fixed`,
29
29
  * `map --max-dirs`, `check --json`, `build [--deep]`, and `--version`.
30
+ *
31
+ * The floor sits on the 0.21 minor line this adapter is verified against
32
+ * (0.21.1). Upgrades are deliberate: an executable below the floor reports
33
+ * `incompatible`, and the adapter never overwrites it — the user upgrades with
34
+ * {@link INSTALL_COMMAND}.
30
35
  */
31
- export const MIN_GRAFT_VERSION = "0.18.0";
36
+ export const MIN_GRAFT_VERSION = "0.21.0";
32
37
  /** Upper bound is exclusive: a 1.x CLI is allowed to change its contract. */
33
38
  export const MAX_GRAFT_VERSION_EXCLUSIVE = "1.0.0";
34
39
 
@@ -45,8 +50,8 @@ export const DEEP_BUILD_TIMEOUT_MS = 1_800_000;
45
50
  /** Global npm installs can be slow on a cold cache. */
46
51
  export const INSTALL_TIMEOUT_MS = 180_000;
47
52
 
48
- /** Kept on the verified 0.18 minor line; never install a floating latest CLI. */
49
- export const GRAFT_INSTALL_SPEC = `${GRAFT_PACKAGE}@^0.18`;
53
+ /** Kept on the verified 0.21 minor line; never install a floating latest CLI. */
54
+ export const GRAFT_INSTALL_SPEC = `${GRAFT_PACKAGE}@^0.21`;
50
55
  export const INSTALL_COMMAND = `npm install -g ${GRAFT_INSTALL_SPEC}`;
51
56
  export const TELEMETRY_ENV = "DO_NOT_TRACK";
52
57
 
@@ -399,7 +404,7 @@ export interface AskHit {
399
404
  }
400
405
 
401
406
  /**
402
- * `graft ask --json` result, as emitted by `AskResult` upstream (0.18.0).
407
+ * `graft ask --json` result, as emitted by `AskResult` upstream (0.21.1).
403
408
  *
404
409
  * Every field is optional on read: a CLI that changes shape degrades to raw
405
410
  * text rather than to a wrong summary, and callers must never treat the parse
@@ -15,11 +15,11 @@ import {
15
15
  import { MODE_ENTRY_TYPE, type GraftState } from "./state.ts";
16
16
  import { notified, makeCtx, makePi, type CtxStub } from "./test-support.ts";
17
17
 
18
- const HEALTHY: GraftState = { name: "fresh-structural", version: "0.18.0" };
18
+ const HEALTHY: GraftState = { name: "fresh-structural", version: "0.21.1" };
19
19
 
20
20
  function installOf(overrides: Partial<GraftInstallRun> = {}): GraftInstallRun {
21
21
  return {
22
- argv: ["npm", "install", "-g", "@nanonets/graft@^0.18"],
22
+ argv: ["npm", "install", "-g", "@nanonets/graft@^0.21"],
23
23
  cwd: "/repo",
24
24
  code: 0,
25
25
  stdout: "",
@@ -75,7 +75,7 @@ function makeRuntime(
75
75
  const recheck = vi.fn(async () => {});
76
76
  const installCli = vi.fn(async () => {
77
77
  const result = overrides.installResult ?? installOf();
78
- if (result.code === 0 && !result.killed) state = overrides.postInstallState ?? { name: "unbuilt", version: "0.18.0" };
78
+ if (result.code === 0 && !result.killed) state = overrides.postInstallState ?? { name: "unbuilt", version: "0.21.1" };
79
79
  return result;
80
80
  });
81
81
  const runBuild = vi.fn(async () => overrides.buildResult ?? runOf());
@@ -134,7 +134,7 @@ describe("formatDoctor", () => {
134
134
  it("reports repository, executable, graph, state, mode, telemetry and provider boundary", () => {
135
135
  const text = formatDoctor(base);
136
136
  expect(text).toContain("/repo (git ✓)");
137
- expect(text).toContain("graft 0.18.0");
137
+ expect(text).toContain("graft 0.21.1");
138
138
  expect(text).toContain("structural only");
139
139
  expect(text).toContain("fresh-structural");
140
140
  expect(text).toContain("nothing is injected");
@@ -188,7 +188,7 @@ describe("formatStatus", () => {
188
188
  providerEnv: [],
189
189
  });
190
190
  expect(text.split("\n")).toHaveLength(4);
191
- expect(text).toContain("graft: ● structural v0.18.0");
191
+ expect(text).toContain("graft: ● structural v0.21.1");
192
192
  expect(text).toContain("hybrid");
193
193
  expect(text).toContain("root: /repo");
194
194
  });
@@ -6,7 +6,7 @@
6
6
  * adapter against the real executable, and it skips itself when no compatible
7
7
  * Graft CLI is on PATH — which is the state of ordinary CI.
8
8
  *
9
- * To run it for real: `npm install -g @nanonets/graft@^0.18` then
9
+ * To run it for real: `npm install -g @nanonets/graft@^0.21` then
10
10
  * npx vitest run src/extensions/pi-graft/graft.integration.test.ts
11
11
  *
12
12
  * A structural build is tree-sitter only: no model, no API key, no network. The
@@ -39,7 +39,7 @@ function writeProjectSettings(value: unknown): void {
39
39
  function exec(graftOutput = ASK_JSON) {
40
40
  return async (command: string, args: string[]) => {
41
41
  if (command === "git" && args.includes("--show-toplevel")) return { ...OK, stdout: `${repo}\n` };
42
- if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
42
+ if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
43
43
  return { ...OK, stdout: graftOutput };
44
44
  };
45
45
  }
@@ -88,7 +88,7 @@ describe("session start", () => {
88
88
  it("resolves the Git working tree, probes the executable, and publishes a status line", async () => {
89
89
  const session = await bootSession();
90
90
  expect(session.commandsRun()).toEqual(["git rev-parse --show-toplevel", "graft --version"]);
91
- expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.18.0");
91
+ expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1");
92
92
  await session.reset();
93
93
  });
94
94
 
@@ -163,7 +163,7 @@ describe("session start", () => {
163
163
  }
164
164
  if (args.includes("--version")) {
165
165
  if (!installed) throw new Error("spawn graft ENOENT");
166
- return { ...OK, stdout: "0.18.0\n" };
166
+ return { ...OK, stdout: "0.21.1\n" };
167
167
  }
168
168
  return { ...OK };
169
169
  });
@@ -171,8 +171,8 @@ describe("session start", () => {
171
171
  const ctx = makeCtx({ cwd: repo });
172
172
  const start = handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>;
173
173
  await start({ type: "session_start", reason: "startup" }, ctx);
174
- await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.18.0"));
175
- expect(harness.commandsRun()).toContain("npm install -g @nanonets/graft@^0.18");
174
+ await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1"));
175
+ expect(harness.commandsRun()).toContain("npm install -g @nanonets/graft@^0.21");
176
176
 
177
177
  await start({ type: "session_start", reason: "reload" }, ctx);
178
178
  expect(harness.commandsRun().filter((command) => command.startsWith("npm install"))).toHaveLength(1);
@@ -202,7 +202,7 @@ describe("session start", () => {
202
202
  writeProjectSettings({});
203
203
  const harness = makePi(async (command: string) => {
204
204
  if (command === "git") return { code: 128, stdout: "", stderr: "not a git repository", killed: false };
205
- return { ...OK, stdout: "0.18.0\n" };
205
+ return { ...OK, stdout: "0.21.1\n" };
206
206
  });
207
207
  graftExtension(harness.pi as never);
208
208
  const ctx = makeCtx({ cwd: repo });
@@ -233,14 +233,14 @@ describe("mutation awareness", () => {
233
233
  it("marks the graph changed after a successful edit", async () => {
234
234
  const session = await bootSession();
235
235
  await onToolResult(session, "edit", { path: "src/a.ts" });
236
- expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.18.0");
236
+ expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.21.1");
237
237
  await session.reset();
238
238
  });
239
239
 
240
240
  it("marks the graph changed after a clearly mutating bash command", async () => {
241
241
  const session = await bootSession();
242
242
  await onToolResult(session, "bash", { command: "rm -rf dist" });
243
- expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.18.0");
243
+ expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.21.1");
244
244
  await session.reset();
245
245
  });
246
246
 
@@ -249,7 +249,7 @@ describe("mutation awareness", () => {
249
249
  await onToolResult(session, "read", { path: "src/a.ts" });
250
250
  await onToolResult(session, "bash", { command: "git status" });
251
251
  await onToolResult(session, "edit", { path: "src/a.ts" }, true);
252
- expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.18.0");
252
+ expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1");
253
253
  await session.reset();
254
254
  });
255
255
  });
@@ -270,7 +270,7 @@ describe("proactive refresh", () => {
270
270
  writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
271
271
  const harness = makePi(async (command: string, args: string[]) => {
272
272
  if (command === "git") return { ...OK, stdout: `${bare}\n` };
273
- if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
273
+ if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
274
274
  if (args.includes("build")) {
275
275
  mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
276
276
  writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
@@ -283,7 +283,7 @@ describe("proactive refresh", () => {
283
283
  { type: "session_start", reason: "startup" },
284
284
  ctx,
285
285
  );
286
- await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.18.0"));
286
+ await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1"));
287
287
  expect(harness.commandsRun()).toContain("graft build");
288
288
  });
289
289
 
@@ -294,7 +294,7 @@ describe("proactive refresh", () => {
294
294
  writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
295
295
  const harness = makePi(async (command: string, args: string[]) => {
296
296
  if (command === "git") return { ...OK, stdout: `${bare}\n` };
297
- if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
297
+ if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
298
298
  if (args.includes("build")) {
299
299
  mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
300
300
  writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
@@ -314,7 +314,7 @@ describe("proactive refresh", () => {
314
314
  { type: "session_start", reason: "startup" },
315
315
  ctx,
316
316
  );
317
- await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.18.0"));
317
+ await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.21.1"));
318
318
  const build = harness.exec.mock.calls.find((call) => (call[1] as string[]).includes("build"))!;
319
319
  expect(build[1]).toEqual(["build", "--deep"]);
320
320
  expect(build[2]).toMatchObject({
@@ -335,7 +335,7 @@ describe("proactive refresh", () => {
335
335
  const models: string[] = [];
336
336
  const harness = makePi(async (command: string, args: string[], options?: unknown) => {
337
337
  if (command === "git") return { ...OK, stdout: `${bare}\n` };
338
- if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
338
+ if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
339
339
  if (args.includes("build")) {
340
340
  const model = (options as { env?: Record<string, string> }).env?.GRAFT_MODEL ?? "";
341
341
  models.push(model);
@@ -359,7 +359,7 @@ describe("proactive refresh", () => {
359
359
  { type: "session_start", reason: "startup" },
360
360
  ctx,
361
361
  );
362
- await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.18.0"));
362
+ await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.21.1"));
363
363
  expect(models).toEqual(["deepseek-v4.1-flash", "glm-5.3-flash"]);
364
364
  expect(notified(ctx)).toContain("retrying the deep pass");
365
365
  });
@@ -371,7 +371,7 @@ describe("proactive refresh", () => {
371
371
  writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
372
372
  const harness = makePi(async (command: string, args: string[]) => {
373
373
  if (command === "git") return { ...OK, stdout: `${bare}\n` };
374
- if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
374
+ if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
375
375
  if (args.includes("build")) {
376
376
  mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
377
377
  writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
@@ -388,7 +388,7 @@ describe("proactive refresh", () => {
388
388
  { type: "session_start", reason: "startup" },
389
389
  ctx,
390
390
  );
391
- await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.18.0"));
391
+ await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1"));
392
392
  const build = harness.exec.mock.calls.find((call) => (call[1] as string[]).includes("build"))!;
393
393
  expect(build[1]).toEqual(["build"]);
394
394
  });
@@ -403,11 +403,11 @@ describe("proactive refresh", () => {
403
403
  { type: "tool_result", toolName: "write", toolCallId: "c1", input: {}, content: [], isError: false },
404
404
  session.ctx,
405
405
  );
406
- expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.18.0");
406
+ expect(session.ctx.statuses.get("graft")).toBe("graft: ◐ changed v0.21.1");
407
407
 
408
408
  await (handlerFor(session, "agent_end") as (e: unknown, c: unknown) => Promise<void>)({}, session.ctx);
409
- expect(session.ctx.statuses.get("graft")).toBe("graft: ⚙ syncing v0.18.0");
410
- await vi.waitFor(() => expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.18.0"));
409
+ expect(session.ctx.statuses.get("graft")).toBe("graft: ⚙ syncing v0.21.1");
410
+ await vi.waitFor(() => expect(session.ctx.statuses.get("graft")).toBe("graft: ● structural v0.21.1"));
411
411
  expect(session.commandsRun()).toContain("graft build");
412
412
  await session.reset();
413
413
  });
@@ -434,7 +434,7 @@ describe("proactive refresh", () => {
434
434
  writeProjectSettings({ refreshDebounceSeconds: 0 });
435
435
  const harness = makePi(async (command: string, args: string[]) => {
436
436
  if (command === "git") return { ...OK, stdout: `${repo}\n` };
437
- if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
437
+ if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
438
438
  if (args.includes("build")) return { code: 1, stdout: "", stderr: "✗ index failed", killed: false };
439
439
  return { ...OK, stdout: ASK_JSON };
440
440
  });
@@ -449,7 +449,7 @@ describe("proactive refresh", () => {
449
449
  ctx,
450
450
  );
451
451
  await (handlerFor(harness, "agent_end") as (e: unknown, c: unknown) => Promise<void>)({}, ctx);
452
- await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ✕ failed v0.18.0"));
452
+ await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ✕ failed v0.21.1"));
453
453
  });
454
454
  });
455
455
 
@@ -541,7 +541,7 @@ describe("retrieval strategy", () => {
541
541
  writeProjectSettings({ mode: "push" });
542
542
  const harness = makePi(async (command: string, args: string[]) => {
543
543
  if (command === "git") return { ...OK, stdout: `${repo}\n` };
544
- if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
544
+ if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
545
545
  return { code: 1, stdout: "", stderr: "✗ graph missing", killed: false };
546
546
  });
547
547
  graftExtension(harness.pi as never);
@@ -565,7 +565,7 @@ describe("retrieval strategy", () => {
565
565
  writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: { mode: "push" } }), "utf-8");
566
566
  const harness = makePi(async (command: string, args: string[]) => {
567
567
  if (command === "git") return { ...OK, stdout: `${bare}\n` };
568
- if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
568
+ if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
569
569
  if (args.includes("build")) {
570
570
  mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
571
571
  writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
@@ -642,7 +642,7 @@ describe("tool readiness", () => {
642
642
  writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
643
643
  const harness = makePi(async (command: string, args: string[]) => {
644
644
  if (command === "git") return { ...OK, stdout: `${bare}\n` };
645
- if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
645
+ if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
646
646
  if (args.includes("build")) {
647
647
  mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
648
648
  writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
@@ -671,7 +671,7 @@ describe("tool readiness", () => {
671
671
  gitCalls += 1;
672
672
  return { code: 128, stdout: "", stderr: "not a git repository", killed: false };
673
673
  }
674
- if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
674
+ if (args.includes("--version")) return { ...OK, stdout: "0.21.1\n" };
675
675
  return { ...OK, stdout: ASK_JSON };
676
676
  });
677
677
  graftExtension(harness.pi as never);
@@ -13,7 +13,7 @@ import {
13
13
  } from "./prompt.ts";
14
14
  import { DEFAULT_MIN_COVERAGE, type GraftState } from "./state.ts";
15
15
 
16
- const FRESH: GraftState = { name: "fresh-structural", version: "0.18.0" };
16
+ const FRESH: GraftState = { name: "fresh-structural", version: "0.21.1" };
17
17
 
18
18
  function payload(overrides: Partial<AskPayload> = {}): AskPayload {
19
19
  return {
@@ -22,8 +22,8 @@ import {
22
22
  } from "./state.ts";
23
23
 
24
24
  const UNBUILT: GraftState = { name: "unbuilt", detail: "no graph yet" };
25
- const FRESH: GraftState = { name: "fresh-structural", version: "0.18.0", deep: false };
26
- const DEEP: GraftState = { name: "fresh-deep", version: "0.18.0", deep: true };
25
+ const FRESH: GraftState = { name: "fresh-structural", version: "0.21.1", deep: false };
26
+ const DEEP: GraftState = { name: "fresh-deep", version: "0.21.1", deep: true };
27
27
 
28
28
  describe("reduceGraftState", () => {
29
29
  it("only claims a repository once root resolution says there is a Git working tree", () => {
@@ -36,16 +36,16 @@ describe("reduceGraftState", () => {
36
36
  });
37
37
 
38
38
  it("derives built/fresh/deep from what is actually on disk", () => {
39
- expect(reduceGraftState(gitState(), { type: "probe-ok", version: "0.18.0", graph: ABSENT_GRAPH })).toEqual({
39
+ expect(reduceGraftState(gitState(), { type: "probe-ok", version: "0.21.1", graph: ABSENT_GRAPH })).toEqual({
40
40
  name: "unbuilt",
41
- version: "0.18.0",
41
+ version: "0.21.1",
42
42
  detail: expect.stringContaining("/graft build"),
43
43
  });
44
44
  expect(
45
- reduceGraftState(gitState(), { type: "probe-ok", version: "0.18.0", graph: { built: true, deep: false } }),
45
+ reduceGraftState(gitState(), { type: "probe-ok", version: "0.21.1", graph: { built: true, deep: false } }),
46
46
  ).toEqual(FRESH);
47
47
  expect(
48
- reduceGraftState(gitState(), { type: "probe-ok", version: "0.18.0", graph: { built: true, deep: true } }),
48
+ reduceGraftState(gitState(), { type: "probe-ok", version: "0.21.1", graph: { built: true, deep: true } }),
49
49
  ).toMatchObject({ name: "fresh-deep", deep: true });
50
50
  });
51
51
 
@@ -91,7 +91,7 @@ describe("reduceGraftState", () => {
91
91
  expect(reduceGraftState(FRESH, { type: "build-started", deep: true })).toMatchObject({
92
92
  name: "building",
93
93
  deep: true,
94
- version: "0.18.0",
94
+ version: "0.21.1",
95
95
  });
96
96
  });
97
97
 
@@ -140,8 +140,8 @@ describe("state predicates and rendering", () => {
140
140
  });
141
141
 
142
142
  it("renders a compact status line with the version when it is known", () => {
143
- expect(statusText(FRESH)).toBe("graft: ● structural v0.18.0");
144
- expect(statusText(DEEP)).toBe("graft: ● deep v0.18.0");
143
+ expect(statusText(FRESH)).toBe("graft: ● structural v0.21.1");
144
+ expect(statusText(DEEP)).toBe("graft: ● deep v0.21.1");
145
145
  expect(statusText({ name: "unavailable" })).toBe("graft: ✕ no cli");
146
146
  expect(statusText({ name: "changed" })).toBe("graft: ◐ changed");
147
147
  });
@@ -190,7 +190,7 @@ export function makeCtx(
190
190
  */
191
191
  export function healthyExec(overrides: { gitRoot?: string; version?: string; queryOutput?: string } = {}): ExecImpl {
192
192
  const gitRoot = overrides.gitRoot ?? "/repo";
193
- const version = overrides.version ?? "0.18.0";
193
+ const version = overrides.version ?? "0.21.1";
194
194
  return async (command, args) => {
195
195
  if (command === "git" && args.includes("--show-toplevel")) return { ...OK, stdout: `${gitRoot}\n` };
196
196
  if (args.includes("--version")) return { ...OK, stdout: `${version}\n` };
@@ -64,7 +64,7 @@ function makeHarness(
64
64
  const runtime: GraftToolRuntime = {
65
65
  run: run as never,
66
66
  settings: () => options.settings ?? {},
67
- state: () => options.state ?? { name: "fresh-structural", version: "0.18.0" },
67
+ state: () => options.state ?? { name: "fresh-structural", version: "0.21.1" },
68
68
  noteQuery: ((_ctx: unknown, result: GraftRun) => {
69
69
  noted.push(result);
70
70
  }) as never,
@@ -143,6 +143,19 @@ See auth.ts:142-156.
143
143
 
144
144
  The reply hint (enabled by default) points to `intercom({ action: "reply", ... })`, so recipients do not need raw sender or `replyTo` IDs. Idle recipients get a new turn immediately; busy interactive recipients receive the message through Selesai's steering queue at the next safe model boundary without aborting the active turn. Attachment content is included in the agent-visible body, and messages are rendered inline and stored in Selesai session history.
145
145
 
146
+ ## Desktop / RPC clients
147
+
148
+ RPC clients can use typed `intercom_status`, `intercom_list`, `intercom_send`, `intercom_ask`, `intercom_reply`, and `intercom_pending` commands directly, without a model turn or the TUI-only `/intercom` overlay. They reuse this extension's broker connection, reply tracker, scope, and consent policy. Ordinary/inferred sends honor `confirmSend`; explicit replies and asks skip confirmation as in the tool. Missing/disabled extensions fail explicitly.
149
+
150
+ ```json
151
+ {"id":"peers","type":"intercom_list"}
152
+ {"id":"send","type":"intercom_send","to":"peer-full-id","message":"Build finished."}
153
+ {"id":"ask","type":"intercom_ask","to":"peer-full-id","message":"Approve?","timeoutMs":60000}
154
+ {"id":"reply","type":"intercom_reply","replyTo":"question-id","message":"Approved."}
155
+ ```
156
+
157
+ Structured connection, peer join/leave, presence, incoming message, delivery, and receipt events stream while idle or busy. Command `id` correlates responses; Intercom `messageId`/`replyTo` correlate messages and receipts. An ask timeout does not cancel delivered work. See [the RPC protocol](../../../docs/rpc.md#intercom-desktop--headless-clients) for response shapes, event examples, timeout limits, confirmation dialogs, and typed client helpers.
158
+
146
159
  ## Workflow: Planner-Worker Coordination
147
160
 
148
161
  The most natural use of pi-intercom is splitting a task between two sessions — one holds the big picture, the other does the hands-on work. When the worker hits an ambiguity ("should I optimize for readability or performance here?"), they ask without losing context.
@@ -610,3 +623,5 @@ Use pi-messenger for multi-agent swarms working on a shared task. Use pi-interco
610
623
  - **No attachments UI** — `file`, `snippet`, and `context` attachments are supported in the protocol, but not in the compose overlay
611
624
  - **Only connected sessions appear** — The list shows Selesai sessions that have loaded `pi-intercom` and successfully registered with the broker, not every open Selesai process on the machine
612
625
  - **Broker lifecycle** — The broker auto-spawns on first use and exits when idle; sessions reconnect automatically if the broker restarts
626
+
627
+ All Intercom events and `intercom_status.data` carry `scope: {version:1,sessionId,epoch}`. `scope.sessionId` is the host parent SDK session ID, not a path or broker endpoint. Existing top-level `sessionId` still identifies broker self (status/connection) or departing peer (leave). The epoch is shared with subagent snapshots and rotates on rebind/reload; discard obsolete scope pairs. Broker reconnect within a binding keeps the epoch.