pi-harness-runtime 1.1.410 → 1.1.412

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 (171) hide show
  1. package/harness/notification-settings.ts +130 -0
  2. package/harness/skill-remote-source.ts +157 -0
  3. package/harness/skill-sync.ts +74 -22
  4. package/harness/skills-startup.ts +202 -0
  5. package/harness/startup-report.ts +98 -56
  6. package/harness-startup-status.ts +31 -2
  7. package/index.ts +125 -31
  8. package/package.json +1 -1
  9. package/packages/a2a-adapter/package.json +1 -1
  10. package/packages/agent-policy/package.json +1 -1
  11. package/packages/architecture-generator/package.json +1 -1
  12. package/packages/auth/package.json +1 -1
  13. package/packages/autonomous-refactor/package.json +1 -1
  14. package/packages/autonomous-runtime/package.json +1 -1
  15. package/packages/autonomous-runtime/test/types.test.ts +59 -32
  16. package/packages/cache-strategy/package.json +1 -1
  17. package/packages/capability-registry/package.json +1 -1
  18. package/packages/checkpoint/package.json +1 -1
  19. package/packages/cli-plugin-sdk/package.json +1 -1
  20. package/packages/clipboard/package.json +1 -1
  21. package/packages/clipboard-plugin/package.json +1 -1
  22. package/packages/code-generation/package.json +1 -1
  23. package/packages/code-review/package.json +1 -1
  24. package/packages/codex-adapter/package.json +1 -1
  25. package/packages/codex-watcher/package.json +1 -1
  26. package/packages/config-capture/package.json +1 -1
  27. package/packages/connection-runtime/package.json +1 -1
  28. package/packages/context-compiler/package.json +1 -1
  29. package/packages/context-discovery/package.json +1 -1
  30. package/packages/context-manager/package.json +1 -1
  31. package/packages/contracts/package.json +1 -1
  32. package/packages/cookie-sanitizer/package.json +1 -1
  33. package/packages/cookie-sanitizer/test/sync.test.ts +7 -1
  34. package/packages/cost-optimizer/package.json +1 -1
  35. package/packages/dependency-analyzer/dist/src/imports.js +3 -3
  36. package/packages/dependency-analyzer/dist/src/imports.js.map +1 -1
  37. package/packages/dependency-analyzer/package.json +1 -1
  38. package/packages/dependency-analyzer/src/imports.ts +3 -3
  39. package/packages/dependency-analyzer/test/analyzer.test.ts +1 -0
  40. package/packages/django-plugin/package.json +1 -1
  41. package/packages/doc-generator/package.json +1 -1
  42. package/packages/durable-review/package.json +1 -1
  43. package/packages/durable-review/test/acceptance.test.ts +24 -2
  44. package/packages/durable-review/test/smoke.test.ts +85 -47
  45. package/packages/e2e-tester/package.json +1 -1
  46. package/packages/evaluation-engine/package.json +1 -1
  47. package/packages/evaluation-runner/package.json +1 -1
  48. package/packages/event-bus/package.json +1 -1
  49. package/packages/event-bus/test/bus.test.ts +16 -4
  50. package/packages/event-sink/package.json +1 -1
  51. package/packages/event-store/package.json +1 -1
  52. package/packages/experience-replay/package.json +1 -1
  53. package/packages/feedback-collector/package.json +1 -1
  54. package/packages/file-copy-helper/package.json +1 -1
  55. package/packages/framework-detector/package.json +1 -1
  56. package/packages/framework-plugin-sdk/package.json +1 -1
  57. package/packages/frappe-plugin/package.json +1 -1
  58. package/packages/generic-web-plugin/package.json +1 -1
  59. package/packages/health-monitor/package.json +1 -1
  60. package/packages/intent-analyzer/package.json +1 -1
  61. package/packages/jev-judge/package.json +1 -1
  62. package/packages/knowledge-graph/package.json +1 -1
  63. package/packages/knowledge-retrieval/package.json +1 -1
  64. package/packages/laravel-plugin/package.json +1 -1
  65. package/packages/learning-engine/package.json +1 -1
  66. package/packages/logging/package.json +1 -1
  67. package/packages/mcp-adapter/package.json +1 -1
  68. package/packages/memory-engine/package.json +1 -1
  69. package/packages/milestone-manager/package.json +1 -1
  70. package/packages/model-registry/package.json +1 -1
  71. package/packages/nextjs-plugin/package.json +1 -1
  72. package/packages/notification/package.json +1 -1
  73. package/packages/observability/package.json +1 -1
  74. package/packages/okf-indexer/package.json +1 -1
  75. package/packages/paperclip-integration/package.json +1 -1
  76. package/packages/performance-optimizer/package.json +1 -1
  77. package/packages/permission-mode/package.json +1 -1
  78. package/packages/privilege-broker/package.json +1 -1
  79. package/packages/project-analyzer/package.json +1 -1
  80. package/packages/project-bootstrap/package.json +1 -1
  81. package/packages/projection-engine/package.json +1 -1
  82. package/packages/prompt-compiler/package.json +1 -1
  83. package/packages/prompt-versioning/package.json +1 -1
  84. package/packages/provider-adapter-sdk/package.json +1 -1
  85. package/packages/provider-router/package.json +1 -1
  86. package/packages/provider-selector/package.json +1 -1
  87. package/packages/providers/package.json +1 -1
  88. package/packages/qdrant-skills/dist/client.d.ts.map +1 -1
  89. package/packages/qdrant-skills/dist/client.js +21 -8
  90. package/packages/qdrant-skills/dist/client.js.map +1 -1
  91. package/packages/qdrant-skills/dist/embedder.d.ts +37 -0
  92. package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -1
  93. package/packages/qdrant-skills/dist/embedder.js +65 -1
  94. package/packages/qdrant-skills/dist/embedder.js.map +1 -1
  95. package/packages/qdrant-skills/dist/index.d.ts +1 -1
  96. package/packages/qdrant-skills/dist/index.d.ts.map +1 -1
  97. package/packages/qdrant-skills/dist/index.js +1 -1
  98. package/packages/qdrant-skills/dist/index.js.map +1 -1
  99. package/packages/qdrant-skills/package.json +1 -1
  100. package/packages/qdrant-skills/src/client.ts +19 -8
  101. package/packages/qdrant-skills/src/embedder.ts +103 -1
  102. package/packages/qdrant-skills/src/index.ts +5 -0
  103. package/packages/qdrant-skills/test/collection-info.test.ts +129 -0
  104. package/packages/qdrant-skills/test/embedder-batch.test.ts +184 -0
  105. package/packages/quota-manager/package.json +1 -1
  106. package/packages/rate-limiter/package.json +1 -1
  107. package/packages/react-vite-plugin/package.json +1 -1
  108. package/packages/relay/package.json +1 -1
  109. package/packages/release-manager/package.json +1 -1
  110. package/packages/requirement-compiler/package.json +1 -1
  111. package/packages/runtime/package.json +1 -1
  112. package/packages/scheduler/package.json +1 -1
  113. package/packages/scheduler-adapter/package.json +1 -1
  114. package/packages/session/package.json +1 -1
  115. package/packages/session-api/package.json +1 -1
  116. package/packages/session-export/package.json +1 -1
  117. package/packages/shared-context/package.json +1 -1
  118. package/packages/skill-mcp-client/package.json +1 -1
  119. package/packages/skill-registry/package.json +1 -1
  120. package/packages/skills/dist/index.d.ts +19 -1
  121. package/packages/skills/dist/index.d.ts.map +1 -1
  122. package/packages/skills/dist/index.js +7 -2
  123. package/packages/skills/dist/index.js.map +1 -1
  124. package/packages/skills/dist/loader.d.ts +273 -3
  125. package/packages/skills/dist/loader.d.ts.map +1 -1
  126. package/packages/skills/dist/loader.js +525 -36
  127. package/packages/skills/dist/loader.js.map +1 -1
  128. package/packages/skills/dist/remote-source.d.ts +107 -0
  129. package/packages/skills/dist/remote-source.d.ts.map +1 -0
  130. package/packages/skills/dist/remote-source.js +79 -0
  131. package/packages/skills/dist/remote-source.js.map +1 -0
  132. package/packages/skills/dist/vector-schema.d.ts +58 -0
  133. package/packages/skills/dist/vector-schema.d.ts.map +1 -0
  134. package/packages/skills/dist/vector-schema.js +55 -0
  135. package/packages/skills/dist/vector-schema.js.map +1 -0
  136. package/packages/skills/package.json +1 -1
  137. package/packages/skills/src/index.ts +28 -5
  138. package/packages/skills/src/loader.ts +688 -53
  139. package/packages/skills/src/remote-source.ts +148 -0
  140. package/packages/skills/src/vector-schema.ts +72 -0
  141. package/packages/skills/test/init-skills.test.ts +107 -111
  142. package/packages/skills/test/remote-skills.test.ts +242 -0
  143. package/packages/skills/test/remote-source.test.ts +248 -0
  144. package/packages/skills/test/vector-indexing.test.ts +205 -0
  145. package/packages/sprint-planner/package.json +1 -1
  146. package/packages/subscription-engine/package.json +1 -1
  147. package/packages/task-compiler/package.json +1 -1
  148. package/packages/telegram/package.json +1 -1
  149. package/packages/tencentdb-memory/package.json +1 -1
  150. package/packages/tencentdb-sync/package.json +1 -1
  151. package/packages/test-data-generator/package.json +1 -1
  152. package/packages/test-generator/package.json +1 -1
  153. package/packages/todo-bd-sync/package.json +1 -1
  154. package/packages/token-estimation/package.json +1 -1
  155. package/packages/token-optimizer/package.json +1 -1
  156. package/packages/tooling-preference/package.json +1 -1
  157. package/packages/trajectory/package.json +1 -1
  158. package/packages/tui/package.json +1 -1
  159. package/packages/two-phase-review/package.json +1 -1
  160. package/packages/types/package.json +1 -1
  161. package/packages/voice-api/package.json +1 -1
  162. package/packages/webmcp-tester/package.json +1 -1
  163. package/packages/wiki/package.json +1 -1
  164. package/packages/workflow-events/package.json +1 -1
  165. package/packages/workspace-scanner/package.json +1 -1
  166. package/packages/workspace-scanner/test/scanner.test.ts +4 -3
  167. package/packages/worktree/package.json +1 -1
  168. package/packages/write-review/package.json +1 -1
  169. package/scripts/sync-to-qdrant.ts +79 -14
  170. package/packages/autonomous-runtime/test/autonomous-runtime.test.ts +0 -194
  171. package/packages/autonomous-runtime/test/inbox.test.ts +0 -80
@@ -13,7 +13,7 @@
13
13
 
14
14
  import type { KeyFileAuditResult } from "./key-file-audit.ts";
15
15
 
16
- export type StartupStatus = "wait" | "ok" | "warn" | "err";
16
+ export type StartupStatus = "wait" | "ok" | "info" | "warn" | "err";
17
17
 
18
18
  export interface StartupReport {
19
19
  /** The label of the startup line ("Jev", "Relay", "LINE"). */
@@ -141,12 +141,28 @@ export function relayEndpointReport(options: {
141
141
  };
142
142
  }
143
143
 
144
+ /**
145
+ * The two steps that create a usable LINE channel.
146
+ *
147
+ * They are shown whenever the credentials are missing: a warning that names
148
+ * only the key files leaves the user without a way to get the values.
149
+ */
150
+ export const LINE_SETUP_STEPS: readonly string[] = [
151
+ "Create a LINE Messaging API channel in LINE Developers Console.",
152
+ "Issue a Long-term channel access token and get Your user ID from Basic settings.",
153
+ ];
154
+
144
155
  /**
145
156
  * The LINE line.
146
157
  *
147
- * Returns null when LINE must not be reported at all: a user who never
148
- * touched LINE must not see a warning on every start. The line appears as
149
- * soon as one of the LINE client key files exists.
158
+ * The line is ALWAYS reported: Telegram and LINE are the two notification
159
+ * channels, and a fresh install must show both of them next to each other -
160
+ * a user who has never heard of LINE cannot miss the channel they do not
161
+ * have yet.
162
+ *
163
+ * "Disabled" is never inferred from the absence of key files. Only an
164
+ * explicit opt-out (the user turned the channel off) produces the
165
+ * informational disabled state.
150
166
  *
151
167
  * The audit is passed in (and not read here) so that the rule "never report
152
168
  * LINE as configured when the audit failed" can be tested.
@@ -154,8 +170,13 @@ export function relayEndpointReport(options: {
154
170
  export function lineConfigurationReport(options: {
155
171
  /** The audit result, or null when the audit could not run. */
156
172
  audit: KeyFileAuditResult | null;
157
- /** Does a LINE client key file exist (line-access-token.txt / line-user-id.txt)? */
158
- clientKeyFilePresent: boolean;
173
+ /**
174
+ * Did the user explicitly turn the LINE channel off?
175
+ *
176
+ * A missing key file is NOT a disabled channel - it is a channel that was
177
+ * never configured.
178
+ */
179
+ disabled?: boolean;
159
180
  /**
160
181
  * The fix commands for Telegram.
161
182
  *
@@ -196,8 +217,22 @@ export function lineConfigurationReport(options: {
196
217
  */
197
218
  channelDiagnostic?: string | null;
198
219
  }): StartupReport | null {
199
- if (!options.clientKeyFilePresent) return null;
200
220
  const channelState = options.channelState ?? "not-started";
221
+
222
+ // An explicit opt-out is an information, not a warning: the user asked
223
+ // for it and the channel works as told.
224
+ if (options.disabled) {
225
+ return {
226
+ label: "LINE",
227
+ text: "disabled",
228
+ status: "info",
229
+ details: [
230
+ "The LINE channel is turned off in the notification settings.",
231
+ "Turn it on again with: /notify-settings line on",
232
+ ],
233
+ };
234
+ }
235
+
201
236
  if (!options.audit) {
202
237
  // The audit failed: LINE must not be reported as configured.
203
238
  const details = options.fixes.length > 0 ? options.fixes : [];
@@ -214,64 +249,71 @@ export function lineConfigurationReport(options: {
214
249
  };
215
250
  }
216
251
  const missing = options.audit.byFeature.get("LINE") ?? [];
217
- if (missing.length === 0) {
218
- if (channelState === "started") {
219
- // The runtime reported the green line itself ("configured and
220
- // enabled") - a second LINE line would only overwrite it.
221
- return null;
222
- }
223
- if (channelState === "failed") {
224
- // The credentials are there but the channel did not start - the
225
- // channel access token is what the startup check validates, so
226
- // the user id must not be named as the cause.
227
- const details = [
228
- ...(options.accessTokenFixes && options.accessTokenFixes.length > 0
229
- ? options.accessTokenFixes
230
- : options.fixes),
231
- ];
232
- if (options.channelDiagnostic) {
233
- details.push(options.channelDiagnostic);
234
- }
235
- details.push(
236
- "The startup check validates the channel access token - the user id is only the recipient of the messages.",
237
- );
238
- details.push(
239
- "Fix the file, then reload Pi so the channel is checked again.",
240
- );
241
- return {
242
- label: "LINE",
243
- text: "configured but the LINE channel did not start",
244
- status: "warn",
245
- details,
246
- };
247
- }
248
- // The credentials are there but the channel was never started in this
249
- // run - say so instead of showing a green line. The cause differs:
250
- // without Telegram the channel cannot start at all, with Telegram the
251
- // credentials were simply created after the harness was loaded.
252
- const cause = options.telegramConfigured
253
- ? "The LINE channel starts together with the Telegram channel - the key files were created after Pi was loaded."
254
- : "The LINE channel starts together with the Telegram channel - it cannot start before Telegram works.";
252
+ if (missing.length > 0) {
253
+ // The credentials are missing (no key file at all, or a file the
254
+ // reader rejects). The warning names the two files AND the two steps
255
+ // that produce their values - otherwise the user is stuck.
255
256
  return {
256
257
  label: "LINE",
257
- // The text names the action - a reload only helps when Telegram
258
- // works; otherwise the user has to configure Telegram first.
259
- text: options.telegramConfigured
260
- ? "configured; reload Pi to enable the channel"
261
- : "configured; Telegram must be configured first",
258
+ text: "credentials missing; notifications unavailable",
262
259
  status: "warn",
263
260
  details: [
264
- cause,
265
- // Without Telegram the user needs the Telegram commands - the
266
- // line must not be the only warning without an action.
267
- ...(options.telegramConfigured ? [] : (options.telegramFixes ?? [])),
261
+ ...LINE_SETUP_STEPS,
262
+ ...options.fixes,
263
+ "Then reload Pi so the channel is checked again.",
268
264
  ],
269
265
  };
270
266
  }
267
+ if (channelState === "started") {
268
+ // The runtime reported the green line itself ("configured and
269
+ // enabled") - a second LINE line would only overwrite it.
270
+ return null;
271
+ }
272
+ if (channelState === "failed") {
273
+ // The credentials are there but the channel did not start - the
274
+ // channel access token is what the startup check validates, so
275
+ // the user id must not be named as the cause.
276
+ const details = [
277
+ ...(options.accessTokenFixes && options.accessTokenFixes.length > 0
278
+ ? options.accessTokenFixes
279
+ : options.fixes),
280
+ ];
281
+ if (options.channelDiagnostic) {
282
+ details.push(options.channelDiagnostic);
283
+ }
284
+ details.push(
285
+ "The startup check validates the channel access token - the user id is only the recipient of the messages.",
286
+ );
287
+ details.push(
288
+ "Fix the file, then reload Pi so the channel is checked again.",
289
+ );
290
+ return {
291
+ label: "LINE",
292
+ text: "configured but the LINE channel did not start",
293
+ status: "warn",
294
+ details,
295
+ };
296
+ }
297
+ // The credentials are there but the channel was never started in this
298
+ // run - say so instead of showing a green line. The cause differs:
299
+ // without Telegram the channel cannot start at all, with Telegram the
300
+ // credentials were simply created after the harness was loaded.
301
+ const cause = options.telegramConfigured
302
+ ? "The LINE channel starts together with the Telegram channel - the key files were created after Pi was loaded."
303
+ : "The LINE channel starts together with the Telegram channel - it cannot start before Telegram works.";
271
304
  return {
272
305
  label: "LINE",
273
- text: "not configured",
306
+ // The text names the action - a reload only helps when Telegram
307
+ // works; otherwise the user has to configure Telegram first.
308
+ text: options.telegramConfigured
309
+ ? "configured; reload Pi to enable the channel"
310
+ : "configured; Telegram must be configured first",
274
311
  status: "warn",
275
- details: [...options.fixes, "Configure LINE in ~/.pi-harness-runtime/keys/, then reload Pi."],
312
+ details: [
313
+ cause,
314
+ // Without Telegram the user needs the Telegram commands - the
315
+ // line must not be the only warning without an action.
316
+ ...(options.telegramConfigured ? [] : (options.telegramFixes ?? [])),
317
+ ],
276
318
  };
277
319
  }
@@ -1,4 +1,4 @@
1
- export type HarnessStatus = "wait" | "ok" | "warn" | "err";
1
+ export type HarnessStatus = "wait" | "ok" | "info" | "warn" | "err";
2
2
 
3
3
  export interface HarnessMessage {
4
4
  id: string;
@@ -20,8 +20,37 @@ export class HarnessStartupStatus {
20
20
  else this.messages[existing] = next;
21
21
  this.onChange?.();
22
22
  }
23
+
24
+ /**
25
+ * Put a message directly after another one.
26
+ *
27
+ * Two rows that belong together stay together: the notification channels
28
+ * (Telegram and LINE) must not be separated by a row that another
29
+ * feature added in between. A message that does not exist yet is
30
+ * inserted; an existing one is moved.
31
+ */
32
+ updateAfter(
33
+ anchorLabel: string,
34
+ label: string,
35
+ text: string,
36
+ status: HarnessStatus = "wait",
37
+ details?: string[],
38
+ ): void {
39
+ const next = { id: label, label, text, status, details };
40
+ const existing = this.messages.findIndex((message) => message.id === label);
41
+ if (existing !== -1) this.messages.splice(existing, 1);
42
+ const anchor = this.messages.findIndex((message) => message.id === anchorLabel);
43
+ if (anchor === -1) {
44
+ // Without the anchor there is nothing to be next to - the row
45
+ // goes to the end instead of being lost.
46
+ this.messages.push(next);
47
+ } else {
48
+ this.messages.splice(anchor + 1, 0, next);
49
+ }
50
+ this.onChange?.();
51
+ }
23
52
  }
24
53
 
25
54
  export function harnessStatusMarker(status: HarnessStatus): string {
26
- return { wait: "[WAIT]", ok: "[OK]", warn: "[WARN]", err: "[ERR]" }[status];
55
+ return { wait: "[WAIT]", ok: "[OK]", info: "[INFO]", warn: "[WARN]", err: "[ERR]" }[status];
27
56
  }
package/index.ts CHANGED
@@ -19,7 +19,7 @@ import type {
19
19
  ExtensionCommandContext,
20
20
  ExtensionContext,
21
21
  } from "@earendil-works/pi-coding-agent";
22
- import { HarnessStartupStatus, harnessStatusMarker } from "./harness-startup-status.ts";
22
+ import { HarnessStartupStatus, harnessStatusMarker, type HarnessStatus } from "./harness-startup-status.ts";
23
23
  import {
24
24
  auditKeyFiles,
25
25
  fallbackKeyFileFixes,
@@ -38,6 +38,8 @@ import {
38
38
  relayEndpointReport,
39
39
  relayShouldBeReported,
40
40
  } from "./harness/startup-report.ts";
41
+ import { localSkillsReport, skillsReport } from "./harness/skills-startup.ts";
42
+ import { isChannelDisabled, setChannelDisabled } from "./harness/notification-settings.ts";
41
43
  import { UsageTracker } from "./tracker.ts";
42
44
  import { MirrorStore, type MirrorRecord } from "./mirror.ts";
43
45
  import { MiniMaxQuotaScraper } from "./harness/e2e/minimax-quota-scraper.js";
@@ -2102,7 +2104,14 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
2102
2104
  if (center.isChannelHealthy("line")) {
2103
2105
  lineChannelState = "started";
2104
2106
  lineChannelDiagnostic = null;
2105
- addHarnessMessage("LINE", "configured and enabled (tap Toggle LINE to disable)", "ok");
2107
+ // The LINE line belongs directly after the Telegram line: the
2108
+ // two notification channels are one topic.
2109
+ addHarnessMessageAfter(
2110
+ "Telegram",
2111
+ "LINE",
2112
+ "configured and enabled (tap Toggle LINE to disable)",
2113
+ "ok",
2114
+ );
2106
2115
  } else {
2107
2116
  lineChannelState = "failed";
2108
2117
  // Keep the reason: "the token is wrong" and "LINE is down"
@@ -2199,8 +2208,14 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
2199
2208
  } else if (parsed.action === "toggle_line") {
2200
2209
  const lineEnabled = (center as any).isChannelEnabled?.("line") ?? true;
2201
2210
  (center as any).setChannelEnabled?.("line", !lineEnabled);
2211
+ // The decision must survive a restart: the startup
2212
+ // summary has to know that the channel was turned off
2213
+ // on purpose.
2214
+ persistChannelDecision("line", !lineEnabled);
2202
2215
  logToFile("[TelegramCallback]", `LINE ${!lineEnabled ? "enabled" : "disabled"}`);
2203
2216
  await acknowledge(`LINE notifications ${!lineEnabled ? "enabled" : "disabled"}`);
2217
+ // The startup line must tell the truth.
2218
+ reportLineConfiguration();
2204
2219
  } else {
2205
2220
  logToFile("[TelegramCallback]", `Unknown action: ${parsed.action}`);
2206
2221
  }
@@ -2223,8 +2238,14 @@ async function initTelegram(pi: ExtensionAPI): Promise<void> {
2223
2238
  const targetChannels = channel === "all" ? ["telegram", "line"] : [channel];
2224
2239
  for (const ch of targetChannels) {
2225
2240
  (center as any).setChannelEnabled?.(ch, isEnabled);
2241
+ // The decision must survive a restart.
2242
+ persistChannelDecision(ch, isEnabled);
2226
2243
  logToFile("[NotifySettings]", `Set ${ch} to ${isEnabled ? "enabled" : "disabled"}`);
2227
2244
  }
2245
+ if (targetChannels.includes("line")) {
2246
+ // The startup line must tell the truth.
2247
+ reportLineConfiguration();
2248
+ }
2228
2249
  }
2229
2250
 
2230
2251
  // Show current status
@@ -2506,6 +2527,48 @@ function reportRelayConfiguration(): void {
2506
2527
  addHarnessMessage(report.label, report.text, report.status, report.details);
2507
2528
  }
2508
2529
 
2530
+ /**
2531
+ * Put a startup line directly after another one.
2532
+ *
2533
+ * The notification channels belong together: the Telegram line and the LINE
2534
+ * line must not be separated by a line that another feature added in
2535
+ * between.
2536
+ */
2537
+ function addHarnessMessageAfter(
2538
+ anchorLabel: string,
2539
+ label: string,
2540
+ text: string,
2541
+ status: HarnessStatus = "wait",
2542
+ details?: string[],
2543
+ ): void {
2544
+ startupStatus.updateAfter(anchorLabel, label, text, status, details);
2545
+ }
2546
+
2547
+ /**
2548
+ * Remember an explicit channel decision.
2549
+ *
2550
+ * The notification center keeps the state in memory (and in its own
2551
+ * database). The startup summary needs the decision BEFORE any channel is
2552
+ * initialized, and it must survive a restart - so it is written to the
2553
+ * notification settings.
2554
+ */
2555
+ function persistChannelDecision(channel: string, enabled: boolean): void {
2556
+ if (!setChannelDisabled(channel, !enabled)) {
2557
+ logToFile("[NotifySettings]", `Could not persist the ${channel} decision`);
2558
+ }
2559
+ }
2560
+
2561
+ /**
2562
+ * Did the user explicitly turn the LINE channel off?
2563
+ *
2564
+ * Only an explicit decision counts. A missing key file means the channel
2565
+ * was never configured - that is a different state with a different
2566
+ * meaning, and it must not be shown as "disabled".
2567
+ */
2568
+ function lineIsExplicitlyDisabled(): boolean {
2569
+ return isChannelDisabled("line");
2570
+ }
2571
+
2509
2572
  /**
2510
2573
  * Report the LINE configuration in the startup summary.
2511
2574
  *
@@ -2514,20 +2577,23 @@ function reportRelayConfiguration(): void {
2514
2577
  * configured - that is exactly the fresh-install case where the user needs
2515
2578
  * the instructions. The "configured" line stays with initTelegram, which owns
2516
2579
  * the channel.
2580
+ *
2581
+ * The line is always reported: a fresh install must show that LINE is one
2582
+ * of the two notification channels, together with the steps that make it
2583
+ * work.
2517
2584
  */
2518
2585
  function reportLineConfiguration(): void {
2519
- // A user who never touched LINE must not see a warning on every start.
2520
- // The line appears as soon as a LINE client key file exists.
2521
2586
  const report = lineConfigurationReport({
2522
2587
  audit: getKeyFileAudit(),
2523
- clientKeyFilePresent: hasLineClientKeyFile(),
2588
+ // An explicit opt-out is the only thing that makes the channel
2589
+ // "disabled".
2590
+ disabled: lineIsExplicitlyDisabled(),
2524
2591
  channelState: lineChannelState,
2525
2592
  telegramConfigured: telegramIsConfigured(),
2526
- // The fallback makes sure that a warning always carries a command,
2527
- // even when the audit could not answer. It names the ACCESS TOKEN
2528
- // only - the user id is the recipient of the messages, not the cause
2529
- // of a failed startup check.
2530
- fixes: keyFileFixesOrFallback("LINE", ["channel access token"]),
2593
+ // Both credentials are named: a LINE channel needs the access token
2594
+ // AND the user id. The fallback makes sure that a warning always
2595
+ // carries a command, even when the audit could not answer.
2596
+ fixes: keyFileFixesOrFallback("LINE", ["channel access token", "user id"]),
2531
2597
  // The startup check validates the access token - the user id is only
2532
2598
  // the recipient, so it must not be named as the cause.
2533
2599
  accessTokenFixes: keyFileFixesOrFallback("LINE", ["channel access token"]),
@@ -2538,7 +2604,7 @@ function reportLineConfiguration(): void {
2538
2604
  telegramFixes: keyFileFixesOrFallback("Telegram", ["bot token", "chat id"]),
2539
2605
  });
2540
2606
  if (!report) return;
2541
- addHarnessMessage(report.label, report.text, report.status, report.details);
2607
+ addHarnessMessageAfter("Telegram", report.label, report.text, report.status, report.details);
2542
2608
  }
2543
2609
 
2544
2610
  /**
@@ -2576,24 +2642,6 @@ function hasAnyRelayKeyFile(): boolean {
2576
2642
  }
2577
2643
  }
2578
2644
 
2579
- /**
2580
- * Does the key directory hold a LINE client credential?
2581
- *
2582
- * The LINE warning is only relevant for a user who started to set LINE up
2583
- * (a line-access-token.txt or a line-user-id.txt file exists) but did not
2584
- * finish. The channel secret (line-channel-secret.txt) does not count: it
2585
- * is only needed on the host that runs the webhook server.
2586
- */
2587
- function hasLineClientKeyFile(): boolean {
2588
- try {
2589
- return readdirSync(harnessKeysDir()).some(
2590
- (entry) => entry === "line-access-token.txt" || entry === "line-user-id.txt",
2591
- );
2592
- } catch {
2593
- return false;
2594
- }
2595
- }
2596
-
2597
2645
  /**
2598
2646
  * Initialize relay streaming for real-time callback delivery.
2599
2647
  * Connects to the relay WebSocket server and forwards Telegram callbacks
@@ -3416,8 +3464,54 @@ async function initHermesSkills(pi: ExtensionAPI): Promise<void> {
3416
3464
 
3417
3465
  // Scan skills from default locations
3418
3466
  const result = initSkills();
3419
- logStartup(`[pi-harness] Scanned ${result.skills.length} skills`);
3420
- addHarnessMessage("Skills", `${result.skills.length} discovered`, "ok");
3467
+ const localCount = result.skills.length;
3468
+ logStartup(`[pi-harness] Scanned ${localCount} local skills`);
3469
+ // The line carries the local count AND the count of the vector
3470
+ // store: a machine without local skill files can still reach a
3471
+ // shared skill, and a single "N discovered" hides that difference.
3472
+ const local = localSkillsReport(localCount);
3473
+ addHarnessMessage("Skills", local.text, local.status, local.details);
3474
+ // The server-held skills. The service holds the authoritative
3475
+ // files, so a machine without a local copy can still use a skill.
3476
+ // It is only used when it is configured - a client without a
3477
+ // service never calls out.
3478
+ let remoteConfigured = false;
3479
+ try {
3480
+ const { isSkillSyncConfigured } = await import(
3481
+ "./harness/skill-sync.js"
3482
+ );
3483
+ remoteConfigured = isSkillSyncConfigured();
3484
+ if (remoteConfigured) {
3485
+ const { SkillServiceSource } = await import(
3486
+ "./harness/skill-remote-source.js"
3487
+ );
3488
+ result.registry.setRemoteSource(new SkillServiceSource());
3489
+ }
3490
+ } catch (err) {
3491
+ logStartup(
3492
+ `[pi-harness] Could not prepare the skill service: ${err instanceof Error ? err.message : String(err)}`,
3493
+ );
3494
+ }
3495
+
3496
+ // The count of the vector store needs the cluster, so it is fetched
3497
+ // in the background - the startup must not wait for the network.
3498
+ void (async () => {
3499
+ const stats = await result.registry.getQdrantStats();
3500
+ // The service is asked for its own state, so a refused
3501
+ // service and an unreachable one stay apart.
3502
+ const remote = remoteConfigured
3503
+ ? await result.registry.getRemoteStats().catch(() => ({
3504
+ state: "unavailable" as const,
3505
+ count: 0,
3506
+ }))
3507
+ : undefined;
3508
+ const report = skillsReport(localCount, stats, remote);
3509
+ addHarnessMessage("Skills", report.text, report.status, report.details);
3510
+ })().catch((err) => {
3511
+ logStartup(
3512
+ `[pi-harness] Could not read the skill vector store: ${err instanceof Error ? err.message : String(err)}`,
3513
+ );
3514
+ });
3421
3515
 
3422
3516
  // Register /skill commands
3423
3517
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-harness-runtime",
3
- "version": "1.1.410",
3
+ "version": "1.1.412",
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.410",
3
+ "version": "1.1.412",
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.410",
3
+ "version": "1.1.412",
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.410",
3
+ "version": "1.1.412",
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.410",
3
+ "version": "1.1.412",
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.410",
3
+ "version": "1.1.412",
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.410",
3
+ "version": "1.1.412",
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,26 +1,29 @@
1
+ /// <reference types="bun-types" />
1
2
  /**
2
3
  * Tests for autonomous-runtime types.
3
4
  */
4
5
 
5
6
  import { describe, it, expect } from "bun:test";
6
7
  import type {
7
- TaskSpec,
8
8
  TaskStatus,
9
9
  TaskEvent,
10
10
  TaskRecord,
11
- LeaseStatus,
12
- WorkerStatus,
11
+ TaskLease,
12
+ WorkerHeartbeat,
13
13
  } from "../src/types.js";
14
14
 
15
- describe("TaskStatus enum", () => {
15
+ describe("TaskStatus", () => {
16
16
  it("has all expected values", () => {
17
17
  const values: TaskStatus[] = [
18
- "pending",
18
+ "queued",
19
19
  "claimed",
20
20
  "running",
21
+ "waiting_approval",
22
+ "waiting_quota",
23
+ "retrying",
21
24
  "completed",
22
25
  "failed",
23
- "cancelled",
26
+ "dead_letter",
24
27
  ];
25
28
  for (const v of values) {
26
29
  expect(typeof v).toBe("string");
@@ -28,41 +31,65 @@ describe("TaskStatus enum", () => {
28
31
  });
29
32
  });
30
33
 
31
- describe("LeaseStatus enum", () => {
32
- it("has all expected values", () => {
33
- const values: LeaseStatus[] = [
34
- "active",
35
- "renewed",
36
- "released",
37
- "expired",
38
- ];
39
- for (const v of values) {
40
- expect(typeof v).toBe("string");
41
- }
34
+ describe("TaskEvent", () => {
35
+ it("accepts a minimal valid event", () => {
36
+ const event: TaskEvent = {
37
+ ts: new Date().toISOString(),
38
+ kind: "started",
39
+ };
40
+ expect(event.kind).toBe("started");
41
+ expect(typeof event.ts).toBe("string");
42
42
  });
43
43
  });
44
44
 
45
- describe("TaskSpec", () => {
46
- it("accepts a minimal valid spec", () => {
47
- const spec: TaskSpec = {
45
+ describe("TaskRecord", () => {
46
+ it("accepts a minimal valid record", () => {
47
+ const now = new Date().toISOString();
48
+ const record: TaskRecord = {
48
49
  id: "task-1",
49
- kind: "once",
50
- priority: 5,
51
- spec: {},
50
+ objective: "Do the thing",
51
+ acceptanceCriteria: ["it is done"],
52
+ source: { kind: "manual", createdBy: "test" },
53
+ priority: 2,
54
+ capabilities: [],
55
+ approvalClass: "automatic_read_only",
56
+ status: "queued",
57
+ attempts: 0,
58
+ maxAttempts: 3,
59
+ createdAt: now,
60
+ updatedAt: now,
61
+ history: [],
62
+ };
63
+ expect(record.id).toBe("task-1");
64
+ expect(record.status).toBe("queued");
65
+ });
66
+ });
67
+
68
+ describe("TaskLease", () => {
69
+ it("accepts a valid lease", () => {
70
+ const now = new Date().toISOString();
71
+ const lease: TaskLease = {
72
+ taskId: "task-1",
73
+ workerId: "worker-1",
74
+ acquiredAt: now,
75
+ expiresAt: now,
76
+ heartbeatAt: now,
77
+ attempt: 1,
52
78
  };
53
- expect(spec.id).toBe("task-1");
54
- expect(spec.kind).toBe("once");
55
- expect(spec.priority).toBe(5);
79
+ expect(lease.workerId).toBe("worker-1");
56
80
  });
57
81
  });
58
82
 
59
- describe("WorkerStatus", () => {
60
- it("accepts a valid status", () => {
61
- const status: WorkerStatus = {
83
+ describe("WorkerHeartbeat", () => {
84
+ it("accepts a valid heartbeat", () => {
85
+ const now = new Date().toISOString();
86
+ const heartbeat: WorkerHeartbeat = {
62
87
  workerId: "worker-1",
63
- running: true,
64
- heartbeatIntervalMs: 30_000,
88
+ startedAt: now,
89
+ lastBeatAt: now,
90
+ capacity: 2,
91
+ inflightTaskIds: [],
65
92
  };
66
- expect(status.running).toBe(true);
93
+ expect(heartbeat.workerId).toBe("worker-1");
67
94
  });
68
95
  });