pi-harness-runtime 1.1.391 → 1.1.392

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 (145) hide show
  1. package/AGENTS.md +29 -8
  2. package/harness/telegram-auth.ts +32 -0
  3. package/harness/telegram-update.ts +125 -0
  4. package/index.ts +100 -27
  5. package/package.json +1 -1
  6. package/packages/a2a-adapter/package.json +1 -1
  7. package/packages/agent-policy/package.json +1 -1
  8. package/packages/architecture-generator/package.json +1 -1
  9. package/packages/auth/package.json +1 -1
  10. package/packages/autonomous-refactor/package.json +1 -1
  11. package/packages/autonomous-runtime/package.json +1 -1
  12. package/packages/cache-strategy/package.json +1 -1
  13. package/packages/capability-registry/package.json +1 -1
  14. package/packages/checkpoint/package.json +1 -1
  15. package/packages/cli-plugin-sdk/package.json +1 -1
  16. package/packages/clipboard/package.json +1 -1
  17. package/packages/clipboard-plugin/package.json +1 -1
  18. package/packages/code-generation/package.json +1 -1
  19. package/packages/code-review/package.json +1 -1
  20. package/packages/codex-adapter/package.json +1 -1
  21. package/packages/codex-watcher/package.json +1 -1
  22. package/packages/config-capture/package.json +1 -1
  23. package/packages/connection-runtime/package.json +1 -1
  24. package/packages/context-compiler/package.json +1 -1
  25. package/packages/context-discovery/package.json +1 -1
  26. package/packages/context-manager/package.json +1 -1
  27. package/packages/contracts/dist/workspace-dotenv.d.ts +44 -0
  28. package/packages/contracts/dist/workspace-dotenv.js +111 -0
  29. package/packages/contracts/dist/workspace-env-format.d.ts +129 -6
  30. package/packages/contracts/dist/workspace-env-format.js +283 -11
  31. package/packages/contracts/dist/workspace-env.d.ts +10 -6
  32. package/packages/contracts/dist/workspace-env.js +36 -92
  33. package/packages/contracts/package.json +1 -1
  34. package/packages/contracts/src/workspace-dotenv.ts +114 -0
  35. package/packages/contracts/src/workspace-env-format.ts +318 -13
  36. package/packages/contracts/src/workspace-env.ts +62 -92
  37. package/packages/contracts/test/workspace-env.test.ts +398 -5
  38. package/packages/cookie-sanitizer/package.json +1 -1
  39. package/packages/cost-optimizer/package.json +1 -1
  40. package/packages/dependency-analyzer/package.json +1 -1
  41. package/packages/django-plugin/package.json +1 -1
  42. package/packages/doc-generator/package.json +1 -1
  43. package/packages/durable-review/package.json +1 -1
  44. package/packages/e2e-tester/package.json +1 -1
  45. package/packages/evaluation-engine/package.json +1 -1
  46. package/packages/evaluation-runner/package.json +1 -1
  47. package/packages/event-bus/package.json +1 -1
  48. package/packages/event-sink/package.json +1 -1
  49. package/packages/event-store/package.json +1 -1
  50. package/packages/experience-replay/package.json +1 -1
  51. package/packages/feedback-collector/package.json +1 -1
  52. package/packages/file-copy-helper/package.json +1 -1
  53. package/packages/framework-detector/package.json +1 -1
  54. package/packages/framework-plugin-sdk/package.json +1 -1
  55. package/packages/frappe-plugin/package.json +1 -1
  56. package/packages/generic-web-plugin/package.json +1 -1
  57. package/packages/health-monitor/package.json +1 -1
  58. package/packages/intent-analyzer/package.json +1 -1
  59. package/packages/jev-judge/package.json +1 -1
  60. package/packages/knowledge-graph/package.json +1 -1
  61. package/packages/knowledge-retrieval/package.json +1 -1
  62. package/packages/laravel-plugin/package.json +1 -1
  63. package/packages/learning-engine/package.json +1 -1
  64. package/packages/mcp-adapter/package.json +1 -1
  65. package/packages/memory-engine/package.json +1 -1
  66. package/packages/milestone-manager/package.json +1 -1
  67. package/packages/model-registry/package.json +1 -1
  68. package/packages/nextjs-plugin/package.json +1 -1
  69. package/packages/notification/dist/adapters/telegram-adapter.d.ts +95 -0
  70. package/packages/notification/dist/adapters/telegram-adapter.d.ts.map +1 -1
  71. package/packages/notification/dist/adapters/telegram-adapter.js +284 -18
  72. package/packages/notification/dist/adapters/telegram-adapter.js.map +1 -1
  73. package/packages/notification/dist/telegram-auth.d.ts +23 -0
  74. package/packages/notification/dist/telegram-auth.d.ts.map +1 -0
  75. package/packages/notification/dist/telegram-auth.js +32 -0
  76. package/packages/notification/dist/telegram-auth.js.map +1 -0
  77. package/packages/notification/dist/types.d.ts +5 -0
  78. package/packages/notification/dist/types.d.ts.map +1 -1
  79. package/packages/notification/package.json +1 -1
  80. package/packages/notification/src/adapters/telegram-adapter.ts +283 -18
  81. package/packages/notification/src/telegram-auth.ts +34 -0
  82. package/packages/notification/src/types.ts +5 -0
  83. package/packages/observability/package.json +1 -1
  84. package/packages/okf-indexer/package.json +1 -1
  85. package/packages/paperclip-integration/package.json +1 -1
  86. package/packages/performance-optimizer/package.json +1 -1
  87. package/packages/permission-mode/package.json +1 -1
  88. package/packages/privilege-broker/package.json +1 -1
  89. package/packages/project-analyzer/package.json +1 -1
  90. package/packages/project-bootstrap/package.json +1 -1
  91. package/packages/projection-engine/package.json +1 -1
  92. package/packages/prompt-compiler/package.json +1 -1
  93. package/packages/prompt-versioning/package.json +1 -1
  94. package/packages/provider-adapter-sdk/package.json +1 -1
  95. package/packages/provider-router/package.json +1 -1
  96. package/packages/provider-selector/package.json +1 -1
  97. package/packages/providers/package.json +1 -1
  98. package/packages/qdrant-skills/package.json +1 -1
  99. package/packages/quota-manager/package.json +1 -1
  100. package/packages/rate-limiter/package.json +1 -1
  101. package/packages/react-vite-plugin/package.json +1 -1
  102. package/packages/relay/dist/server.js +7 -2
  103. package/packages/relay/dist/types.d.ts +3 -1
  104. package/packages/relay/dist/ws-server.d.ts +2 -9
  105. package/packages/relay/dist/ws-server.js +5 -3
  106. package/packages/relay/package.json +1 -1
  107. package/packages/relay/src/server.ts +7 -2
  108. package/packages/relay/src/types.ts +6 -1
  109. package/packages/relay/src/ws-server.ts +9 -13
  110. package/packages/relay/test/relay-e2e.test.ts +36 -0
  111. package/packages/release-manager/package.json +1 -1
  112. package/packages/requirement-compiler/package.json +1 -1
  113. package/packages/runtime/package.json +1 -1
  114. package/packages/scheduler/package.json +1 -1
  115. package/packages/scheduler-adapter/package.json +1 -1
  116. package/packages/session/package.json +1 -1
  117. package/packages/session-api/package.json +1 -1
  118. package/packages/session-export/package.json +1 -1
  119. package/packages/shared-context/package.json +1 -1
  120. package/packages/skill-mcp-client/package.json +1 -1
  121. package/packages/skill-registry/package.json +1 -1
  122. package/packages/skills/package.json +1 -1
  123. package/packages/sprint-planner/package.json +1 -1
  124. package/packages/subscription-engine/package.json +1 -1
  125. package/packages/task-compiler/package.json +1 -1
  126. package/packages/telegram/package.json +1 -1
  127. package/packages/tencentdb-memory/package.json +1 -1
  128. package/packages/tencentdb-sync/package.json +1 -1
  129. package/packages/test-data-generator/package.json +1 -1
  130. package/packages/test-generator/package.json +1 -1
  131. package/packages/todo-bd-sync/package.json +1 -1
  132. package/packages/token-estimation/package.json +1 -1
  133. package/packages/token-optimizer/package.json +1 -1
  134. package/packages/trajectory/package.json +1 -1
  135. package/packages/tui/package.json +1 -1
  136. package/packages/types/package.json +1 -1
  137. package/packages/voice-api/package.json +1 -1
  138. package/packages/webmcp-tester/package.json +1 -1
  139. package/packages/wiki/package.json +1 -1
  140. package/packages/workflow-events/package.json +1 -1
  141. package/packages/workspace-scanner/package.json +1 -1
  142. package/packages/worktree/package.json +1 -1
  143. package/packages/write-review/package.json +1 -1
  144. package/scripts/unified-webhook-server.ts +141 -14
  145. package/scripts/ws-server.ts +7 -1
package/AGENTS.md CHANGED
@@ -494,27 +494,45 @@ bun run build && bun run start # Server deployment
494
494
  ### 14. Workspace Environment Auto-Detection
495
495
 
496
496
  **At session start, the harness auto-detects:**
497
- - `.env` file presence → Dev environment evidence
497
+ - `.env` / `.env.*` file variables → Dev environment evidence
498
498
  - `package.json` `dev` script → Hot reload capability
499
- - `NODE_ENV` value → development/production/test
499
+ - The environment variable, in this order: the `NODE_ENV` of the process env
500
+ (it decides, also when it names no known mode), then the dotenv files of the
501
+ workspace: `NODE_ENV`, an environment shaped key whose value names a known
502
+ mode (`*_ENV`, `*_ENVIRONMENT`, `*_MODE`, `*_STAGE`, e.g. `AMOS_ENV`), then
503
+ any other environment shaped key - and only then an environment shaped key
504
+ of the process env → development/production/test
500
505
  - System RAM → Low RAM warning (<16GB total or <4GB available)
501
506
  - Running processes → dev/build server status
502
507
 
508
+ The dotenv files are read from the WORKSPACE (not from the current working
509
+ directory), the template files (`.env.example`, `.env.sample`,
510
+ `.env.template`, ...) are ignored and a file bigger than 256 kB is not read.
511
+ A value that is not provably harmless is masked (`<set>`): only the
512
+ `NODE_ENV` of the process env and a value that IS the name of the mode are
513
+ shown. A key that only looks like a branch (`BRANCH=production`) never
514
+ decides the mode.
515
+
503
516
  **This info is shown in UI and delivered to agent context:**
504
517
  ```
505
- Workspace: ~/pi-harness-runtime | OS: Linux | mode: DEVELOPMENT | dev server: running | dev script: available | build script: available | RAM: 10/16
518
+ Workspace: ~/pi-harness-runtime | OS: Linux | mode: DEVELOPMENT (from AMOS_ENV in .env) | dev server: running | dev script: available | build script: available | RAM: 10/16
506
519
  ```
507
520
 
508
521
  The mode words are exactly what the detector emits: `DEVELOPMENT`, `PRODUCTION`,
509
- `TEST` or `UNKNOWN` (with its reason, e.g. `UNKNOWN (no explicit NODE_ENV)`).
522
+ `TEST` or `UNKNOWN` (with its reason, e.g.
523
+ `UNKNOWN (no NODE_ENV or environment variable found)` or
524
+ `UNKNOWN (AMOS_ENV in .env names no known mode)`). A known mode also
525
+ names its source: `DEVELOPMENT (from AMOS_ENV in .env)`.
510
526
  A dev/build script is a CAPABILITY, not a state: `dev script: available` means
511
527
  the script exists, not that a server runs. The server state is separate and
512
528
  never claims more than it knows: `running`, `not detected` (a check ran and
513
529
  found nothing) or `not checked` (no check was possible, e.g. no `pgrep`).
514
530
 
515
531
  The OS is part of the line because it decides which command prints the
516
- working directory: `pwd` on Linux/macOS, `cd` or `echo %cd%` on Windows.
517
- The agent context names the command to use.
532
+ working directory: `pwd` on Linux/macOS, `(pwd).Path` or `$pwd.Path` in
533
+ PowerShell (the plain `pwd` there prints a "Path: " header), `cd` or
534
+ `echo %cd%` in the Windows command prompt. The agent context names the
535
+ command to use.
518
536
 
519
537
  **IMPORTANT Rules:**
520
538
 
@@ -528,8 +546,11 @@ The agent context names the command to use.
528
546
 
529
547
  **Decision flow:**
530
548
  ```
531
- Check process.env.NODE_ENV first
532
- Check .env file NODE_ENV second
549
+ Check process.env.NODE_ENV first (it decides, also when it names no known mode)
550
+ Check the .env and .env.* files of the workspace second
551
+ (NODE_ENV, then an environment shaped key whose value names a mode,
552
+ then any other environment shaped key)
553
+ Only then an environment shaped key of the process env (e.g. AMOS_ENV)
533
554
  Presence of dev script = capability, not proof of mode
534
555
  UNKNOWN requires investigation, not auto-build
535
556
  Production detection ≠ deployment authorization
@@ -90,6 +90,38 @@ export function getAllowedUserIds(): Set<number> {
90
90
  return new Set();
91
91
  }
92
92
 
93
+ /**
94
+ * Describe who may write into Pi through Telegram.
95
+ *
96
+ * The allow-list falls back to the chat id file, and a GROUP chat id is
97
+ * negative - it identifies a chat, not a person. A sender rule that only
98
+ * knows a group id lets nobody in, silently. The description makes that
99
+ * visible.
100
+ */
101
+ export function describeInboundAccess(
102
+ allowedUserIds: Iterable<number>,
103
+ chatId?: string,
104
+ ): string {
105
+ const ids = [...allowedUserIds].filter((n) => Number.isFinite(n));
106
+ const users = ids.filter((n) => n > 0);
107
+ const groups = ids.filter((n) => n < 0);
108
+ if (users.length === 0) {
109
+ const known = groups.length > 0 ? ` (the allow-list holds the group chat id ${groups.join(", ")})` : "";
110
+ return `nobody may write in${known}`;
111
+ }
112
+ const chat = chatId ? ` in chat ${chatId}` : "";
113
+ return `user id(s) ${users.join(", ")} may write in${chat}`;
114
+ }
115
+
116
+ /**
117
+ * Decide whether a Telegram sender may write into Pi.
118
+ *
119
+ * The rule itself lives in the notification package
120
+ * (packages/notification/src/telegram-auth.ts) so that the adapter, this
121
+ * module and the remote webhook server all use ONE implementation.
122
+ */
123
+ export { isAuthorizedTelegramSender } from "../packages/notification/dist/telegram-auth.js";
124
+
93
125
  /**
94
126
  * Check if a Telegram user ID is authorized
95
127
  */
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Map a relay "telegram_message" payload onto a Telegram update.
3
+ *
4
+ * The mapping lives in a pure module so that it can be tested without the
5
+ * harness: the relay carries the text, the adapter decides whether the
6
+ * sender is allowed to write in.
7
+ *
8
+ * Rules:
9
+ * - the text is never put into an id field (ids are logged by the client)
10
+ * - the real bot flag has to survive the transport, otherwise the adapter
11
+ * could not refuse bot messages
12
+ * - a payload without text produces no update
13
+ */
14
+
15
+ export interface TelegramMessageUpdate {
16
+ message: {
17
+ message_id: number;
18
+ from: {
19
+ id: number;
20
+ is_bot: boolean;
21
+ first_name: string;
22
+ username?: string;
23
+ };
24
+ chat: { id: number };
25
+ text: string;
26
+ };
27
+ }
28
+
29
+ export interface TelegramMessagePayload {
30
+ userId: number;
31
+ username?: string;
32
+ messageId?: number;
33
+ chatId?: number;
34
+ text?: string;
35
+ isBot?: boolean;
36
+ }
37
+
38
+ export interface TelegramCallbackUpdate {
39
+ callback_query: {
40
+ id: string;
41
+ from: {
42
+ id: number;
43
+ is_bot: boolean;
44
+ first_name: string;
45
+ username?: string;
46
+ };
47
+ data: string;
48
+ message: {
49
+ chat: { id: number };
50
+ message_id: number;
51
+ };
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Build the Telegram update for a relayed button callback.
57
+ *
58
+ * The real bot flag has to survive the transport: the adapter refuses
59
+ * messages and callbacks of a bot.
60
+ */
61
+ export function buildTelegramCallbackUpdate(
62
+ payload: TelegramMessagePayload & {
63
+ action?: string;
64
+ targetId?: string;
65
+ callbackQueryId?: string;
66
+ id?: string;
67
+ },
68
+ ): TelegramCallbackUpdate | null {
69
+ const userId = Number(payload.userId);
70
+ if (!Number.isFinite(userId)) return null;
71
+ const chatId = Number(payload.chatId);
72
+ const effectiveChatId = Number.isFinite(chatId) ? chatId : userId;
73
+ return {
74
+ callback_query: {
75
+ id: payload.callbackQueryId || String(payload.id || ""),
76
+ from: {
77
+ id: userId,
78
+ is_bot: payload.isBot === true,
79
+ first_name: payload.username || "User",
80
+ username: payload.username,
81
+ },
82
+ data: `${payload.action || ""}:${payload.targetId || ""}`,
83
+ message: {
84
+ chat: { id: effectiveChatId },
85
+ message_id: Number(payload.messageId) || 0,
86
+ },
87
+ },
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Build the Telegram update for a relayed text message.
93
+ *
94
+ * Returns null when the payload carries no text - there is nothing to
95
+ * forward and the adapter must not see an empty message.
96
+ */
97
+ export function buildTelegramMessageUpdate(
98
+ payload: TelegramMessagePayload,
99
+ ): TelegramMessageUpdate | null {
100
+ const text = payload.text ?? "";
101
+ if (!text) return null;
102
+
103
+ const userId = Number(payload.userId);
104
+ if (!Number.isFinite(userId)) return null;
105
+
106
+ // A direct message has the chat id of the user; the remote always sends
107
+ // the real chat id, so the fallback is only for a DM.
108
+ const chatId = Number(payload.chatId);
109
+ const effectiveChatId = Number.isFinite(chatId) ? chatId : userId;
110
+
111
+ return {
112
+ message: {
113
+ message_id: Number(payload.messageId) || 0,
114
+ from: {
115
+ id: userId,
116
+ // The real flag: the adapter refuses bot messages.
117
+ is_bot: payload.isBot === true,
118
+ first_name: payload.username || "User",
119
+ username: payload.username,
120
+ },
121
+ chat: { id: effectiveChatId },
122
+ text,
123
+ },
124
+ };
125
+ }
package/index.ts CHANGED
@@ -28,6 +28,8 @@ import {
28
28
  type KeyFileAuditResult,
29
29
  } from "./harness/key-file-audit.ts";
30
30
  import { renderDetailLines } from "./harness/startup-lines.ts";
31
+ import { describeInboundAccess, getAllowedUserIds } from "./harness/telegram-auth.ts";
32
+ import { buildTelegramCallbackUpdate, buildTelegramMessageUpdate } from "./harness/telegram-update.ts";
31
33
  import {
32
34
  jevMissingKeyReport,
33
35
  lineConfigurationReport,
@@ -1364,6 +1366,7 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
1364
1366
  return;
1365
1367
  }
1366
1368
 
1369
+ const allowedUserIds = [...getAllowedUserIds()];
1367
1370
  const channels: Array<{
1368
1371
  id: string;
1369
1372
  type: string;
@@ -1374,10 +1377,27 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
1374
1377
  id: "telegram",
1375
1378
  type: "telegram",
1376
1379
  enabled: true,
1377
- config: { botToken, chatId },
1380
+ config: { botToken, chatId, allowedUserIds },
1378
1381
  },
1379
1382
  ];
1380
1383
 
1384
+ // Who may write into Pi? A group chat id is not a user id: when the
1385
+ // allow-list holds only a group id, the inbound path is closed - and
1386
+ // that must be visible, not silent.
1387
+ const inboundAccess = describeInboundAccess(allowedUserIds, chatId);
1388
+ logStartup(`[pi-harness] Telegram inbound: ${inboundAccess}`);
1389
+ if (inboundAccess.startsWith("nobody")) {
1390
+ addHarnessMessage(
1391
+ "Telegram",
1392
+ "nobody may write in: the allow-list holds no user id",
1393
+ "warn",
1394
+ [
1395
+ "Write your Telegram user id into ~/.pi-harness-runtime/keys/telegram-allowed-users.txt (a chat id of a group is not a user id).",
1396
+ "Then reload Pi.",
1397
+ ],
1398
+ );
1399
+ }
1400
+
1381
1401
  // Add LINE channel if credentials exist
1382
1402
  const lineChannelConfigured = Boolean(hasLineCredentials && lineAccessToken && lineUserId);
1383
1403
  if (lineChannelConfigured) {
@@ -1587,7 +1607,9 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
1587
1607
  return;
1588
1608
  }
1589
1609
 
1590
- logToFile("[TelegramInbound]", `Forwarding message to Pi: "${text.slice(0, 50)}..."`);
1610
+ // The user's own words stay out of the log files: only the length
1611
+ // is recorded.
1612
+ logToFile("[TelegramInbound]", `Forwarding message to Pi (${text.length} chars)`);
1591
1613
  // Forward the message to Pi as a user message
1592
1614
  await pi.sendUserMessage(text, { deliverAs: "steer" });
1593
1615
  });
@@ -1644,7 +1666,32 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
1644
1666
  ]);
1645
1667
  return;
1646
1668
  }
1647
- telegram?.startPolling();
1669
+ // Polling and the remote webhook are mutually exclusive. Telegram
1670
+ // rejects getUpdates while a webhook is active, and the rejected poll
1671
+ // also terminates the getUpdates stream of every other poller of this
1672
+ // bot, which delayed the inbound text messages by seconds.
1673
+ if (telegram) {
1674
+ // A 409 (a webhook or another poller owns this bot) stops the
1675
+ // inbound path. The user must see it - a silent 409 looks like a
1676
+ // working inbound path.
1677
+ telegram.onPollStopped = (reason) => {
1678
+ addHarnessMessage("Telegram", `inbound polling stopped: ${reason}`, "warn", [
1679
+ "Telegram rejects getUpdates (409) while a webhook or another poller owns this bot.",
1680
+ "Stop the other poller of this bot (or remove the remote webhook), then reload Pi.",
1681
+ ]);
1682
+ };
1683
+ if (!telegram.isWebhookConfigured()) {
1684
+ telegram.startPolling();
1685
+ // The poll may already have been rejected (a 409 fires the
1686
+ // callback above); make the reason visible either way.
1687
+ const stopReason = telegram.getPollStopReason();
1688
+ if (stopReason) {
1689
+ addHarnessMessage("Telegram", `inbound polling stopped: ${stopReason}`, "warn", [
1690
+ "Stop the other poller of this bot (or remove the remote webhook), then reload Pi.",
1691
+ ]);
1692
+ }
1693
+ }
1694
+ }
1648
1695
  if (!telegram?.isGreetingVerified()) {
1649
1696
  addHarnessMessage("Telegram", "Hi sent; tap Say Hi to verify inbound (tap Toggle Telegram to disable)", "wait");
1650
1697
  }
@@ -1931,33 +1978,57 @@ async function initRelayStream(): Promise<void> {
1931
1978
  logToFile("[RelayStream]", "Synchronized - receiving live events");
1932
1979
  },
1933
1980
  onCallback: async (callback: CallbackPayload) => {
1934
- logToFile("[RelayStream]", `Callback: ${callback.action} ${callback.targetId}`);
1981
+ logToFile(
1982
+ "[RelayStream]",
1983
+ callback.type === "telegram_message"
1984
+ ? `Message from ${callback.username || callback.userId} (${(callback.text || "").length} chars)`
1985
+ : `Callback: ${callback.action} ${callback.targetId}`,
1986
+ );
1935
1987
 
1936
1988
  // Forward Telegram callbacks to the adapter
1937
1989
  if (callback.type === "telegram_callback" && _relayTelegramAdapter) {
1938
- // Convert relay callback to Telegram update format
1939
- const update = {
1940
- callback_query: {
1941
- id: callback.callbackQueryId || String(callback.id),
1942
- from: {
1943
- id: callback.userId,
1944
- is_bot: false,
1945
- first_name: callback.username || "User",
1946
- username: callback.username,
1947
- },
1948
- data: `${callback.action}:${callback.targetId}`,
1949
- message: {
1950
- chat: { id: callback.chatId || callback.userId },
1951
- message_id: callback.messageId || 0,
1952
- },
1953
- },
1954
- };
1990
+ // The mapping is a pure function (harness/telegram-update.ts) so
1991
+ // that it can be tested without the harness.
1992
+ const update = buildTelegramCallbackUpdate({
1993
+ id: callback.id,
1994
+ action: callback.action,
1995
+ targetId: callback.targetId,
1996
+ callbackQueryId: callback.callbackQueryId,
1997
+ userId: callback.userId,
1998
+ username: callback.username,
1999
+ messageId: callback.messageId,
2000
+ chatId: callback.chatId,
2001
+ isBot: callback.isBot,
2002
+ });
2003
+ if (update) {
2004
+ try {
2005
+ await _relayTelegramAdapter.handleWebhookUpdate(update);
2006
+ logToFile("[RelayStream]", "Callback processed by Telegram adapter");
2007
+ } catch (err) {
2008
+ logToFile("[RelayStream]", `Adapter error: ${oneLineError(err)}`);
2009
+ }
2010
+ }
2011
+ }
1955
2012
 
1956
- try {
1957
- await _relayTelegramAdapter.handleWebhookUpdate(update);
1958
- logToFile("[RelayStream]", "Callback processed by Telegram adapter");
1959
- } catch (err) {
1960
- logToFile("[RelayStream]", `Adapter error: ${err}`);
2013
+ // Forward a Telegram TEXT message (the user typed it) to the adapter
2014
+ if (callback.type === "telegram_message" && _relayTelegramAdapter) {
2015
+ // The mapping is a pure function (harness/telegram-update.ts) so
2016
+ // that it can be tested without the harness.
2017
+ const update = buildTelegramMessageUpdate({
2018
+ userId: callback.userId,
2019
+ username: callback.username,
2020
+ messageId: callback.messageId,
2021
+ chatId: callback.chatId,
2022
+ text: callback.text,
2023
+ isBot: callback.isBot,
2024
+ });
2025
+ if (update) {
2026
+ try {
2027
+ await _relayTelegramAdapter.handleWebhookUpdate(update);
2028
+ logToFile("[RelayStream]", "Message processed by Telegram adapter");
2029
+ } catch (err) {
2030
+ logToFile("[RelayStream]", `Adapter error: ${oneLineError(err)}`);
2031
+ }
1961
2032
  }
1962
2033
  }
1963
2034
  },
@@ -2708,8 +2779,10 @@ const HARNESS_LOG_FILE = "/tmp/pi-harness.log";
2708
2779
  /** Log a harness message to file (not stderr - bypasses Pi's cursor tracking) */
2709
2780
  function logToFile(...parts: string[]): void {
2710
2781
  const msg = parts.join(" ");
2782
+ // A timestamp makes the log usable for latency measurements.
2783
+ const stamp = new Date().toISOString();
2711
2784
  try {
2712
- appendFileSync(HARNESS_LOG_FILE, msg + "\n");
2785
+ appendFileSync(HARNESS_LOG_FILE, `[${stamp}] ${msg}\n`);
2713
2786
  } catch {
2714
2787
  // Ignore file write errors
2715
2788
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-runtime",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "description": "[BETA] Codex-style /usage status + autonomous coding harness for pi. Not production ready — expect breaking changes.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/a2a-adapter",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "description": "Google Agent-to-Agent (A2A) protocol adapter for pi-harness",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/agent-policy",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "description": "Agent policy runtime for pi-harness - enforces mandatory rule delivery and mutation gating",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/architecture-generator",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "description": "Generate ADR and Mermaid diagrams from project analysis",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/auth",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "description": "pi-harness-runtime — auth",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/autonomous-refactor",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "description": "Autonomous refactoring engine",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/autonomous-runtime",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "description": "Autonomous Operations Runtime — task inbox, lease management, and worker lifecycle (RFC-0101)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/cache-strategy",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "description": "Smart caching with TTL and invalidation strategies (RFC-0022)",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/capability-registry",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "description": "Capability Registry for pi-harness-runtime — tracks model capabilities and enables capability-based routing",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/checkpoint",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "description": "Enhanced checkpoint system with incremental snapshots, diff-based storage, and recovery strategies",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/cli-plugin-sdk",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "description": "CLI Plugin SDK for pi-harness — install, list, remove, update, invoke plugins",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/clipboard",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/clipboard-plugin",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/code-generation",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "description": "Code generation pipeline for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/code-review",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "description": "Automated code review engine for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/codex-adapter",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "description": "OpenAI Codex provider adapter for pi-harness",
6
6
  "main": "dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/codex-watcher",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "description": "Watches Codex CLI sessions and emits visible messages for PING-PONG monitoring",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moocoding/config-capture",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "description": "Auto-capture API-fetched configuration values to AGENTS.md",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/connection-runtime",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "description": "Connection management with jittered exponential backoff for pi-harness",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/context-compiler",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "description": "Bounded context selection engine for pi-harness-runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi/context-discovery",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/context-manager",
3
- "version": "1.1.391",
3
+ "version": "1.1.392",
4
4
  "description": "Context Window Manager — prevents model context overflow via compaction and resume prompts (RFC-0010)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Reading the dotenv files of a workspace.
3
+ *
4
+ * This module owns the file system part of the environment detection: it
5
+ * finds the ".env" files of a workspace, reads them and skips what must not
6
+ * be read (templates, huge files, unreadable files).
7
+ *
8
+ * It is deliberately free of any other dependency (no node:sqlite) so it
9
+ * can be unit tested everywhere: the detection module itself needs
10
+ * node:sqlite, which not every runtime provides.
11
+ */
12
+ /**
13
+ * The biggest dotenv file that is read. A ".env" file is a few kilobytes;
14
+ * anything bigger is not a configuration file (and reading it on every
15
+ * session start would be a waste).
16
+ */
17
+ export declare const DOTENV_MAX_BYTES: number;
18
+ /**
19
+ * True for a template file that only documents the variables
20
+ * (".env.example", ".env.sample", ".env.template", ...).
21
+ *
22
+ * A template is not the configuration of the workspace: it must never
23
+ * decide the mode. The marker is looked for in EVERY segment of the
24
+ * suffix, so ".env.example.local" is a template too.
25
+ */
26
+ export declare function isTemplateDotenvName(name: string): boolean;
27
+ /**
28
+ * The dotenv files of a workspace as FULL paths, in the order they are read.
29
+ *
30
+ * ".env" comes first, then the well known variants, then every other
31
+ * ".env.*" file. Template files are skipped.
32
+ */
33
+ export declare function listDotenvFiles(workspacePath: string): string[];
34
+ /**
35
+ * Read the dotenv files of a workspace.
36
+ *
37
+ * The files are read through the full paths of listDotenvFiles, so the
38
+ * workspace - not the current working directory - decides which files are
39
+ * read.
40
+ */
41
+ export declare function readDotenvFiles(workspacePath: string): {
42
+ name: string;
43
+ content: string;
44
+ }[];