@plannotator/pi-extension 0.28.48 → 0.28.49

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.
@@ -331,12 +331,12 @@ export interface PlannotatorConfig {
331
331
  /**
332
332
  * The `plannotator` agent tool (packages/shared/plannotator-tool.ts) on the
333
333
  * hosts that register it: the Claude Code mod, Pi (and the Pi extension in
334
- * Oh My Pi) and OpenCode 2. `true` puts it in the agent's tool list, `false`
335
- * keeps it out (the slash commands are unchanged either way). Read once when
336
- * a session starts, so a change applies to the next session.
337
- * PLANNOTATOR_AGENT_TOOL wins over this key. Unset: the host's default,
338
- * AGENT_TOOL_DEFAULTS (on for the Claude Code mod and Oh My Pi, off on Pi
339
- * and OpenCode 2).
334
+ * Oh My Pi), OpenCode 2 and DeepSeek Harness. `true` puts it in the agent's
335
+ * tool list, `false` keeps it out (the slash commands are unchanged either
336
+ * way). Read once when a session starts, so a change applies to the next
337
+ * session. PLANNOTATOR_AGENT_TOOL wins over this key. Unset: the host's
338
+ * default, AGENT_TOOL_DEFAULTS (on for the Claude Code mod and Oh My Pi, off
339
+ * on Pi, OpenCode 2 and DeepSeek Harness).
340
340
  */
341
341
  agentTool?: boolean;
342
342
  /**
@@ -425,7 +425,8 @@ export interface PlannotatorConfig {
425
425
  experimental?: { decisionMode?: boolean };
426
426
  /**
427
427
  * The Board's one-time first-visit dialog (approval "inbox-board", intro
428
- * amendment): `shownAt` once a page on this computer claimed it
428
+ * amendment): `shownAt` once a page on this computer or the owner's phone
429
+ * claimed it (or the Board reveal showed it)
429
430
  * (`POST /api/inbox/board/intro`, a compare-and-set), once per machine.
430
431
  */
431
432
  inboxBoardIntro?: { shownAt?: string };
@@ -453,12 +454,61 @@ export interface PlannotatorConfig {
453
454
  inboxSnoozeIntro?: { firstUseAt?: string; snoozedAt?: string; shownAt?: string };
454
455
  /**
455
456
  * The Board (approval "inbox-board", 2026-10-26 amendment): something the
456
- * person turns on, off by default. On: the sidebar's Board row and page,
457
- * and the Inbox serves the agents' `card` tool. Off: neither, every card
458
- * kept. Written by the Inbox's Settings → Capabilities (`POST
459
- * /api/inbox/settings` `{ board: { on } }`), read on every request.
457
+ * person turns on. On: the sidebar's Board row and page, and the Inbox
458
+ * serves the agents' `card` tool. Off: neither, every card kept. Written by
459
+ * the Inbox's Settings → Capabilities (`POST /api/inbox/settings`
460
+ * `{ board: { on } }`), read on every request. Three readings (approval
461
+ * "inbox-feature-reveal"): unset = not decided yet (off, and the only state
462
+ * the reveal can turn on), `true` = on, `false` = the person switched it
463
+ * off. The reveal's claim is the only automatic write, unset -> `true`.
460
464
  */
461
465
  inboxBoard?: boolean;
466
+ /**
467
+ * Thread steps (approval "inbox-thread-steps", 2026-11-03): something the
468
+ * person turns on, off by default. On: the thread's steps button, the
469
+ * steps in the Board's opened card, and the agents' `steps` tool, wake
470
+ * line, compaction line and Tidy rule. Off: none of it, every list kept.
471
+ * Written by the Inbox's Settings → Capabilities (`POST
472
+ * /api/inbox/settings` `{ steps: { on } }`), read on every request.
473
+ */
474
+ inboxSteps?: boolean;
475
+ /**
476
+ * Code review in the Inbox (approval "inbox-code-review-direction",
477
+ * 2026-11-03): something the person turns on, off by default. On: the
478
+ * review skills in the reply box's "/" list, the review card and its
479
+ * full-window review with Approve / Send feedback, and submit_guide's
480
+ * `review` field. Off: none of it; guided reviews and `plannotator review`
481
+ * stay as they are. Three readings, like the Board's: unset = not decided
482
+ * yet (off), `true` = on, `false` = the person switched it off. Written by
483
+ * the Inbox's Settings → Capabilities (`POST /api/inbox/settings`
484
+ * `{ code_review: { on } }`), read on every request.
485
+ */
486
+ inboxCodeReview?: boolean;
487
+ /**
488
+ * Shape in the Inbox (approval "shape-inbox-placement"): living boards an
489
+ * agent draws and keeps current. Off unless the person turns it on. On: the
490
+ * thread header's Pebble, the sidebar's Shape row (3+ boards), the Shape
491
+ * group in the search, the `/api/shapes` routes and the shape skill in the
492
+ * reply box's "/" list. Off: none of it, and no agent is told of it. Three
493
+ * readings, like the Board's: unset (off), `true`, `false`. Written by
494
+ * Settings → Capabilities (`POST /api/inbox/settings` `{ shape: { on } }`).
495
+ */
496
+ inboxShape?: boolean;
497
+ /**
498
+ * The queue of one-time reveals for returning users (approval
499
+ * "inbox-feature-reveal"; rules in packages/shared/inbox/reveals.ts). Written
500
+ * only by the server. `lastShownAt`: the last one-time announcement claimed
501
+ * on this machine (list options, voice, Snooze, the Board), the 48 h
502
+ * spacing's clock. `waitingSince`: when each reveal first became next, for
503
+ * the 3-day patience. `board.shownAt`: the Board reveal was claimed.
504
+ * `steps.shownAt`: the Checklist reveal was claimed.
505
+ */
506
+ inboxReveals?: {
507
+ lastShownAt?: string;
508
+ waitingSince?: { snooze?: string; board?: string; steps?: string };
509
+ board?: { shownAt?: string };
510
+ steps?: { shownAt?: string };
511
+ };
462
512
  inboxSound?: {
463
513
  enabled?: boolean;
464
514
  volume?: number;
@@ -613,6 +663,14 @@ export interface PlannotatorConfig {
613
663
  * preference cookies, #2184); remote sessions ignore those names.
614
664
  */
615
665
  urlHost?: string;
666
+ /**
667
+ * Publish every `plannotator review`, `annotate` and `annotate-last`/`last`
668
+ * session over the tailnet, as if `--tailscale` were passed, so a host or
669
+ * agent need not add the flag each time. PLANNOTATOR_TAILSCALE wins over
670
+ * this key; the flag turns it on for one run. Other subcommands ignore it.
671
+ * The Inbox has its own key (`inboxTailscale`). Default: false.
672
+ */
673
+ tailscale?: boolean;
616
674
  /**
617
675
  * Mirror the approved plan checklist into an editable todo provider during
618
676
  * execution (issue #484). "auto" (default) syncs whenever a provider is
@@ -1360,7 +1418,7 @@ export function resolveClaudeCodeMod(
1360
1418
  * from plain Pi itself (`apps/pi-extension/pi-host.ts`): it is the same
1361
1419
  * extension and the same session bridge (host `pi`), with its own default.
1362
1420
  */
1363
- export type AgentToolHost = "claude-code" | "pi" | "oh-my-pi" | "opencode";
1421
+ export type AgentToolHost = "claude-code" | "pi" | "oh-my-pi" | "opencode" | "dsh";
1364
1422
 
1365
1423
  /**
1366
1424
  * Whether each host registers the `plannotator` agent tool when nothing is
@@ -1368,8 +1426,10 @@ export type AgentToolHost = "claude-code" | "pi" | "oh-my-pi" | "opencode";
1368
1426
  * mod (the tool is deferred behind tool search there, so it costs only its
1369
1427
  * name until used) and for Oh My Pi (owner's call 2026-10-12: OMP lists an
1370
1428
  * extension tool as one line in its system prompt, about 50 tokens, and loads
1371
- * the schema only when the model reads it); off on Pi and OpenCode 2 (a full
1372
- * tool definition, about 800 tokens, in every request). The mod's mirror
1429
+ * the schema only when the model reads it); off on Pi, OpenCode 2 and
1430
+ * DeepSeek Harness (`dsh`, the `@plannotator/dsh` plugin; owner's call
1431
+ * 2026-11-02): a full tool definition, about 800 tokens, in every request,
1432
+ * and DSH keeps its tool list fixed for request caching. The mod's mirror
1373
1433
  * (`apps/hook/hooks/mod/enabled.ts`) is kept equal to the claude-code entry by
1374
1434
  * `enabled.test.ts`.
1375
1435
  */
@@ -1378,6 +1438,7 @@ export const AGENT_TOOL_DEFAULTS: Readonly<Record<AgentToolHost, boolean>> = {
1378
1438
  pi: false,
1379
1439
  "oh-my-pi": true,
1380
1440
  opencode: false,
1441
+ dsh: false,
1381
1442
  };
1382
1443
 
1383
1444
  /**
@@ -1436,14 +1497,13 @@ export type InboxToolHost = "claude-code" | "pi" | "opencode" | "dsh";
1436
1497
  * nothing is set: the agent tool's rule (on for the Claude Code mod, where
1437
1498
  * tools are deferred behind tool search; off on Pi, OpenCode 2 and DeepSeek
1438
1499
  * Harness, which send every tool's full definition with each request; DSH
1439
- * also keeps its tool list fixed for request caching). DSH has no
1440
- * `plannotator` agent tool yet, so its entry is its own, off like Pi's.
1500
+ * also keeps its tool list fixed for request caching).
1441
1501
  */
1442
1502
  export const INBOX_TOOL_DEFAULTS: Readonly<Record<InboxToolHost, boolean>> = {
1443
1503
  "claude-code": AGENT_TOOL_DEFAULTS["claude-code"],
1444
1504
  pi: AGENT_TOOL_DEFAULTS.pi,
1445
1505
  opencode: AGENT_TOOL_DEFAULTS.opencode,
1446
- dsh: false,
1506
+ dsh: AGENT_TOOL_DEFAULTS.dsh,
1447
1507
  };
1448
1508
 
1449
1509
  export const INBOX_TOOL_HOSTS: readonly InboxToolHost[] = ["claude-code", "pi", "opencode", "dsh"];
@@ -1664,6 +1724,46 @@ export function resolveInboxTailscale(
1664
1724
  return { on: false, source: "default" };
1665
1725
  }
1666
1726
 
1727
+ export type SessionTailscaleSource = "flag" | "env" | "config" | "default";
1728
+
1729
+ /**
1730
+ * Resolve whether a review / annotate / annotate-last session is published
1731
+ * over the tailnet (the per-session `--tailscale` transport, not the Inbox's).
1732
+ *
1733
+ * Priority (highest wins):
1734
+ * `--tailscale` → PLANNOTATOR_TAILSCALE → config.tailscale → default false
1735
+ *
1736
+ * The flag only turns it on. The env var takes the same words as
1737
+ * PLANNOTATOR_INBOX_TAILSCALE: `1` / `true` / `on` turn it on, `0` / `false` /
1738
+ * `off` / `disabled` keep it off against the config key (a one-run opt-out);
1739
+ * an empty or unrecognized value counts as unset.
1740
+ */
1741
+ export function resolveSessionTailscale(
1742
+ config: PlannotatorConfig,
1743
+ env: NodeJS.ProcessEnv = process.env,
1744
+ flag = false,
1745
+ ): { on: boolean; source: SessionTailscaleSource } {
1746
+ if (flag) return { on: true, source: "flag" };
1747
+ const v = env.PLANNOTATOR_TAILSCALE?.trim().toLowerCase();
1748
+ if (v === "1" || v === "true" || v === "on") return { on: true, source: "env" };
1749
+ if (v === "0" || v === "false" || v === "off" || v === "disabled") return { on: false, source: "env" };
1750
+ const fromConfig = parseConfigBoolean(config.tailscale);
1751
+ if (fromConfig !== undefined) return { on: fromConfig, source: "config" };
1752
+ return { on: false, source: "default" };
1753
+ }
1754
+
1755
+ /**
1756
+ * How to run one session without the tailnet, worded for what turned it on:
1757
+ * dropping the flag does nothing when the env var or config key decided.
1758
+ */
1759
+ export function sessionTailscaleOptOut(source: SessionTailscaleSource): string {
1760
+ return source === "flag"
1761
+ ? "run the same command without --tailscale"
1762
+ : source === "env"
1763
+ ? "run the same command with PLANNOTATOR_TAILSCALE unset or set to 0"
1764
+ : "run the same command with PLANNOTATOR_TAILSCALE=0 (config.json's \"tailscale\" key turned it on)";
1765
+ }
1766
+
1667
1767
  /**
1668
1768
  * config.inboxTailscaleAllow, normalized: trimmed, lower-cased (logins are
1669
1769
  * compared case-insensitively), deduplicated. Entries that are not strings,
@@ -6,9 +6,9 @@
6
6
  import type { GuideViewerAssets } from "./guide-format.ts";
7
7
 
8
8
  export const GUIDE_VIEWER_MANIFEST: Omit<GuideViewerAssets, "baseUrl"> = {
9
- js: "viewer.D8SS6c3e.js",
9
+ js: "viewer.BZxaYwuG.js",
10
10
  css: "viewer.BTKuQZGP.css",
11
- jsIntegrity: "sha384-Bz0RpgVczCqZ8dygRuv+RdRoAs9K7MyKHprrwrC2ZSDT76n5cbtKMPhP3neekody",
11
+ jsIntegrity: "sha384-ePGbxJypqNlC5Dxx7heFv5OK1PvvxkzJYLOWnek0szk065nlTi816+1dQ47ndLPA",
12
12
  cssIntegrity: "sha384-D0ugA4xJZl9NZisbmBfoi17BEAj8FqUmL/qNQa2ecghlvrmiMaiBIxlkeKGhRFgp",
13
13
  langs: {
14
14
  "astro": "chunks/astro.Ts5EKq2l.js",
@@ -49,6 +49,7 @@ export const INBOX_TOOL_ACTIONS = [
49
49
  'submit_guide',
50
50
  'get_guide_brief',
51
51
  'card',
52
+ 'steps',
52
53
  ] as const
53
54
 
54
55
  export type InboxToolAction = (typeof INBOX_TOOL_ACTIONS)[number]
@@ -238,6 +239,9 @@ export function inboxSessionThreadsText(result: unknown, toolName: string = INBO
238
239
  const open = typeof questions?.open === 'number' && questions.open > 0 ? questions.open : 0
239
240
  if (open > 0) parts.push(`${open} open question${open === 1 ? '' : 's'}`)
240
241
  if (thread!.unread === true) parts.push('the person wrote something you have not read')
242
+ // Your steps there (approval "inbox-thread-steps"): the Inbox lists them only while the person has steps on.
243
+ const steps = thread!.steps as { done?: unknown; total?: unknown } | undefined
244
+ if (typeof steps?.done === 'number' && typeof steps.total === 'number' && steps.total > 0) parts.push(`steps ${steps.done}/${steps.total}`)
241
245
  const at = time(thread!.last_at)
242
246
  if (at) parts.push(`last ${thread!.last_author === 'person' ? 'the person' : 'an agent'} ${at}`)
243
247
  lines.push(parts.join(' · '))
@@ -388,6 +392,44 @@ export const INBOX_TOOL_HELP_NOTE = "Every field and question syntax: `plannotat
388
392
  */
389
393
  export const INBOX_TOOL_BOARD_OFF_NOTE = "The Board (cards for work that outlasts a reply) is off until the person turns it on; see `plannotator card --help`."
390
394
 
395
+ /**
396
+ * Thread steps (approval "inbox-thread-steps", 2026-11-03): the owner's
397
+ * approved text word for word, after `steps: `. It rides the `steps` field's
398
+ * description (a field description does not count toward Claude Code's 2,048
399
+ * characters; the owner chose this over a cut description) and is the MCP
400
+ * tool's description. Served only while the person has steps on (config.json
401
+ * `inboxSteps`): off, no agent text carries any of it.
402
+ */
403
+ export const INBOX_STEPS_LEAD =
404
+ 'your plan for the work in this thread, as the person should see it: 3 to 8 short steps, the whole list each call (it replaces the last one). status: todo, doing, done, waiting (on the person), dropped (say why in note). Call it when a step starts or ends; it sends nothing and notifies no one. Skip it for work that ends in this reply.'
405
+
406
+ /** The agent tool's line for the `steps` action (the owner, 2026-11-03): short, so the description stays under 2,048. */
407
+ export const INBOX_STEPS_ACTION_LINE = "keep the person's checklist for this thread current (details on the steps field)."
408
+
409
+ /** The `steps` field (the action's, and send_message's while steps are on): the approved text, then where a list goes before there is a thread. */
410
+ export const INBOX_STEPS_FIELD_DESCRIPTION = `steps: ${INBOX_STEPS_LEAD} No thread yet: send it with the send_message that starts one.`
411
+
412
+ /**
413
+ * The wake's one line on a reply in a thread where the session keeps steps
414
+ * (approval "inbox-thread-steps", approved text): "Your steps here: 3 of 6
415
+ * done; doing: X. Update them if that changed." Without a step in progress,
416
+ * the `doing` part is left out.
417
+ */
418
+ export function inboxStepsWakeLine(steps: { done: number; total: number; doing: string | null }): string {
419
+ return `Your steps here: ${steps.done} of ${steps.total} done${steps.doing ? `; doing: ${steps.doing}` : ''}. Update them if that changed.`
420
+ }
421
+
422
+ /** A command's `steps` as the wake may print it, or null (absent, or not the Inbox's shape). */
423
+ function wakeSteps(value: unknown): { done: number; total: number; doing: string | null } | null {
424
+ if (!value || typeof value !== 'object') return null
425
+ const { done, total, doing } = value as Record<string, unknown>
426
+ const count = (n: unknown): n is number => typeof n === 'number' && Number.isInteger(n) && n >= 0 && n <= 12
427
+ if (!count(done) || !count(total) || total === 0 || done > total) return null
428
+ // One line, no control or separator characters: it goes into the session's prompt.
429
+ const step = typeof doing === 'string' ? doing.replace(/[\p{Cc}\p{Zl}\p{Zp}]+/gu, ' ').replace(/\s+/g, ' ').trim().slice(0, 80) : ''
430
+ return { done, total, doing: step || null }
431
+ }
432
+
391
433
  /**
392
434
  * The agent tool's own line for an action whose Inbox lead is long (the
393
435
  * description must stay under Claude Code's 2,048 characters, and every Pi and
@@ -401,6 +443,7 @@ export const INBOX_AGENT_ACTION_LEADS: Readonly<Record<string, string>> = {
401
443
  search: 'Find messages by words, older ones too (read-only).',
402
444
  get_guide_brief: "How to write a guided review, with a worked submit_guide call.",
403
445
  card: "A Board card for work that outlasts this reply: create it, then move it and say where it is as the work moves.",
446
+ steps: INBOX_STEPS_ACTION_LINE,
404
447
  }
405
448
 
406
449
  /**
@@ -927,6 +970,13 @@ export interface InboxReplyCommand {
927
970
  * message and the body still says what it is. Absent on every other command.
928
971
  */
929
972
  tidy?: boolean
973
+ /**
974
+ * The receiving session's steps in this thread (approval
975
+ * "inbox-thread-steps"), only while the person has steps on and the session
976
+ * keeps a list there: the wake adds `inboxStepsWakeLine`. Absent otherwise;
977
+ * an older connection ignores it.
978
+ */
979
+ steps?: { done: number; total: number; doing: string | null }
930
980
  }
931
981
 
932
982
  /**
@@ -1234,6 +1284,7 @@ export function inboxWakeText(
1234
1284
  reask_items?: unknown
1235
1285
  reask_one?: unknown
1236
1286
  tidy?: boolean
1287
+ steps?: unknown
1237
1288
  },
1238
1289
  toolName: string = INBOX_TOOL_NAME,
1239
1290
  ): string {
@@ -1246,7 +1297,9 @@ export function inboxWakeText(
1246
1297
  const thread = wakeThreadId(command.thread_id)
1247
1298
  return `Plannotator Inbox: ${ackSubject} (${command.id})\n${inboxAckLine(command.ack_of)}${thread ? `\n\n[thread ${thread}]` : ''}`
1248
1299
  }
1249
- const closing = `\n\n${inboxWakeClosingLine(command.thread_id, toolName)}`
1300
+ // The session's steps in this thread (approval "inbox-thread-steps"), its own paragraph before the closing line.
1301
+ const steps = wakeSteps(command.steps)
1302
+ const closing = `${steps ? `\n\n${inboxStepsWakeLine(steps)}` : ''}\n\n${inboxWakeClosingLine(command.thread_id, toolName)}`
1250
1303
  const subject = (command.subject ?? '').replace(/\s+/g, ' ').trim() || (command.type === 'message' ? 'a message' : 'a reply')
1251
1304
  // The thread was handed to another session: the notice alone.
1252
1305
  if (command.handed_away && typeof command.handed_away === 'object') {
@@ -0,0 +1,10 @@
1
+ // @generated — DO NOT EDIT. Source: packages/shared/inbox/project-key.ts
2
+ import { createHash } from "node:crypto";
3
+ import { sanitizeTag } from '../project.ts';
4
+
5
+ /** `<name>-<6 hex of the root>`, so two repositories with one name stay apart. */
6
+ export function inboxProjectKey(name: string, root: string, hashLength = 6): string {
7
+ const base = sanitizeTag(name) ?? "project";
8
+ const hash = createHash("sha256").update(root).digest("hex").slice(0, Math.min(hashLength, 64));
9
+ return `${base}-${hash}`;
10
+ }