pi-harness-runtime 1.1.411 → 1.1.413

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 (157) 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 +229 -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/cache-strategy/package.json +1 -1
  16. package/packages/capability-registry/package.json +1 -1
  17. package/packages/checkpoint/package.json +1 -1
  18. package/packages/cli-plugin-sdk/package.json +1 -1
  19. package/packages/clipboard/package.json +1 -1
  20. package/packages/clipboard-plugin/package.json +1 -1
  21. package/packages/code-generation/package.json +1 -1
  22. package/packages/code-review/package.json +1 -1
  23. package/packages/codex-adapter/package.json +1 -1
  24. package/packages/codex-watcher/package.json +1 -1
  25. package/packages/config-capture/package.json +1 -1
  26. package/packages/connection-runtime/package.json +1 -1
  27. package/packages/context-compiler/package.json +1 -1
  28. package/packages/context-discovery/package.json +1 -1
  29. package/packages/context-manager/package.json +1 -1
  30. package/packages/contracts/package.json +1 -1
  31. package/packages/cookie-sanitizer/package.json +1 -1
  32. package/packages/cost-optimizer/package.json +1 -1
  33. package/packages/dependency-analyzer/package.json +1 -1
  34. package/packages/django-plugin/package.json +1 -1
  35. package/packages/doc-generator/package.json +1 -1
  36. package/packages/durable-review/package.json +1 -1
  37. package/packages/e2e-tester/package.json +1 -1
  38. package/packages/evaluation-engine/package.json +1 -1
  39. package/packages/evaluation-runner/package.json +1 -1
  40. package/packages/event-bus/package.json +1 -1
  41. package/packages/event-sink/package.json +1 -1
  42. package/packages/event-store/package.json +1 -1
  43. package/packages/experience-replay/package.json +1 -1
  44. package/packages/feedback-collector/package.json +1 -1
  45. package/packages/file-copy-helper/package.json +1 -1
  46. package/packages/framework-detector/package.json +1 -1
  47. package/packages/framework-plugin-sdk/package.json +1 -1
  48. package/packages/frappe-plugin/package.json +1 -1
  49. package/packages/generic-web-plugin/package.json +1 -1
  50. package/packages/health-monitor/package.json +1 -1
  51. package/packages/intent-analyzer/package.json +1 -1
  52. package/packages/jev-judge/package.json +1 -1
  53. package/packages/knowledge-graph/package.json +1 -1
  54. package/packages/knowledge-retrieval/package.json +1 -1
  55. package/packages/laravel-plugin/package.json +1 -1
  56. package/packages/learning-engine/package.json +1 -1
  57. package/packages/logging/package.json +1 -1
  58. package/packages/mcp-adapter/package.json +1 -1
  59. package/packages/memory-engine/package.json +1 -1
  60. package/packages/milestone-manager/package.json +1 -1
  61. package/packages/model-registry/package.json +1 -1
  62. package/packages/nextjs-plugin/package.json +1 -1
  63. package/packages/notification/package.json +1 -1
  64. package/packages/observability/package.json +1 -1
  65. package/packages/okf-indexer/package.json +1 -1
  66. package/packages/paperclip-integration/package.json +1 -1
  67. package/packages/performance-optimizer/package.json +1 -1
  68. package/packages/permission-mode/package.json +1 -1
  69. package/packages/privilege-broker/package.json +1 -1
  70. package/packages/project-analyzer/package.json +1 -1
  71. package/packages/project-bootstrap/package.json +1 -1
  72. package/packages/projection-engine/package.json +1 -1
  73. package/packages/prompt-compiler/package.json +1 -1
  74. package/packages/prompt-versioning/package.json +1 -1
  75. package/packages/provider-adapter-sdk/package.json +1 -1
  76. package/packages/provider-router/package.json +1 -1
  77. package/packages/provider-selector/package.json +1 -1
  78. package/packages/providers/package.json +1 -1
  79. package/packages/qdrant-skills/dist/client.d.ts +10 -0
  80. package/packages/qdrant-skills/dist/client.d.ts.map +1 -1
  81. package/packages/qdrant-skills/dist/client.js +41 -9
  82. package/packages/qdrant-skills/dist/client.js.map +1 -1
  83. package/packages/qdrant-skills/dist/embedder.d.ts +10 -0
  84. package/packages/qdrant-skills/dist/embedder.d.ts.map +1 -1
  85. package/packages/qdrant-skills/dist/embedder.js +12 -2
  86. package/packages/qdrant-skills/dist/embedder.js.map +1 -1
  87. package/packages/qdrant-skills/dist/index.d.ts +2 -2
  88. package/packages/qdrant-skills/dist/index.d.ts.map +1 -1
  89. package/packages/qdrant-skills/dist/index.js +2 -2
  90. package/packages/qdrant-skills/dist/index.js.map +1 -1
  91. package/packages/qdrant-skills/package.json +1 -1
  92. package/packages/qdrant-skills/src/client.ts +38 -9
  93. package/packages/qdrant-skills/src/embedder.ts +14 -2
  94. package/packages/qdrant-skills/src/index.ts +3 -0
  95. package/packages/qdrant-skills/test/collection-info.test.ts +165 -0
  96. package/packages/quota-manager/package.json +1 -1
  97. package/packages/rate-limiter/package.json +1 -1
  98. package/packages/react-vite-plugin/package.json +1 -1
  99. package/packages/relay/package.json +1 -1
  100. package/packages/release-manager/package.json +1 -1
  101. package/packages/requirement-compiler/package.json +1 -1
  102. package/packages/runtime/package.json +1 -1
  103. package/packages/scheduler/package.json +1 -1
  104. package/packages/scheduler-adapter/package.json +1 -1
  105. package/packages/session/package.json +1 -1
  106. package/packages/session-api/package.json +1 -1
  107. package/packages/session-export/package.json +1 -1
  108. package/packages/shared-context/package.json +1 -1
  109. package/packages/skill-mcp-client/package.json +1 -1
  110. package/packages/skill-registry/package.json +1 -1
  111. package/packages/skills/dist/index.d.ts +14 -1
  112. package/packages/skills/dist/index.d.ts.map +1 -1
  113. package/packages/skills/dist/index.js +3 -1
  114. package/packages/skills/dist/index.js.map +1 -1
  115. package/packages/skills/dist/loader.d.ts +209 -0
  116. package/packages/skills/dist/loader.d.ts.map +1 -1
  117. package/packages/skills/dist/loader.js +386 -15
  118. package/packages/skills/dist/loader.js.map +1 -1
  119. package/packages/skills/dist/remote-source.d.ts +107 -0
  120. package/packages/skills/dist/remote-source.d.ts.map +1 -0
  121. package/packages/skills/dist/remote-source.js +79 -0
  122. package/packages/skills/dist/remote-source.js.map +1 -0
  123. package/packages/skills/dist/vector-schema.d.ts +58 -0
  124. package/packages/skills/dist/vector-schema.d.ts.map +1 -0
  125. package/packages/skills/dist/vector-schema.js +55 -0
  126. package/packages/skills/dist/vector-schema.js.map +1 -0
  127. package/packages/skills/package.json +1 -1
  128. package/packages/skills/src/index.ts +16 -2
  129. package/packages/skills/src/loader.ts +532 -39
  130. package/packages/skills/src/remote-source.ts +148 -0
  131. package/packages/skills/src/vector-schema.ts +72 -0
  132. package/packages/skills/test/remote-skills.test.ts +292 -0
  133. package/packages/skills/test/remote-source.test.ts +248 -0
  134. package/packages/sprint-planner/package.json +1 -1
  135. package/packages/subscription-engine/package.json +1 -1
  136. package/packages/task-compiler/package.json +1 -1
  137. package/packages/telegram/package.json +1 -1
  138. package/packages/tencentdb-memory/package.json +1 -1
  139. package/packages/tencentdb-sync/package.json +1 -1
  140. package/packages/test-data-generator/package.json +1 -1
  141. package/packages/test-generator/package.json +1 -1
  142. package/packages/todo-bd-sync/package.json +1 -1
  143. package/packages/token-estimation/package.json +1 -1
  144. package/packages/token-optimizer/package.json +1 -1
  145. package/packages/tooling-preference/package.json +1 -1
  146. package/packages/trajectory/package.json +1 -1
  147. package/packages/tui/package.json +1 -1
  148. package/packages/two-phase-review/package.json +1 -1
  149. package/packages/types/package.json +1 -1
  150. package/packages/voice-api/package.json +1 -1
  151. package/packages/webmcp-tester/package.json +1 -1
  152. package/packages/wiki/package.json +1 -1
  153. package/packages/workflow-events/package.json +1 -1
  154. package/packages/workspace-scanner/package.json +1 -1
  155. package/packages/worktree/package.json +1 -1
  156. package/packages/write-review/package.json +1 -1
  157. package/scripts/sync-to-qdrant.ts +79 -14
@@ -0,0 +1,229 @@
1
+ /**
2
+ * The startup line of the skill system.
3
+ *
4
+ * The line carries TWO counts: the skills on this machine and the skills in
5
+ * the vector store. They are different things - a machine without local
6
+ * skill files can still reach a shared skill - and a single "N discovered"
7
+ * hides exactly that difference.
8
+ *
9
+ * The vector store has states that must not be flattened into "0": no
10
+ * credentials, an unreachable cluster and an empty collection are three
11
+ * different situations and need three different actions.
12
+ */
13
+
14
+ /** What the vector store is in this run. */
15
+ export type QdrantSkillState =
16
+ /** No credentials for the vector store on this machine. */
17
+ | "not-configured"
18
+ /** The cluster is configured but cannot be reached. */
19
+ | "unavailable"
20
+ /** The cluster is reachable but the collection does not exist yet. */
21
+ | "missing"
22
+ /** The collection exists but holds no skill. */
23
+ | "empty"
24
+ /** The collection holds skills. */
25
+ | "populated";
26
+
27
+ /** The state of the skill vector store. */
28
+ export interface QdrantSkillStats {
29
+ /** What the vector store is in this run. */
30
+ state: QdrantSkillState;
31
+ /** The number of skill vectors in the collection. */
32
+ count: number;
33
+ /** The collection the numbers belong to. */
34
+ collection?: string;
35
+ /** The vector size the collection was created with. */
36
+ dimensions?: number;
37
+ /** The distance the collection was created with. */
38
+ distance?: string;
39
+ /** Why the collection could not be read. Never a credential. */
40
+ error?: string;
41
+ /** The collection does not match the schema of the runtime. */
42
+ schemaMismatch?: string;
43
+ /**
44
+ * The collection of an earlier installation that still holds the skills.
45
+ *
46
+ * The runtime collection is empty while the legacy one holds the skills
47
+ * of the previous version. They are not lost - they live in a
48
+ * collection with another name, and one with another vector size.
49
+ */
50
+ legacy?: {
51
+ /** The name of the collection. */
52
+ collection: string;
53
+ /** The number of skill vectors it holds. */
54
+ count: number;
55
+ /** The vector size it was created with. */
56
+ dimensions?: number;
57
+ /** The embedding model it was created with. */
58
+ model?: string;
59
+ };
60
+ }
61
+
62
+ export interface SkillsStartupReport {
63
+ /** The text of the line. */
64
+ text: string;
65
+ /** The status of the line. */
66
+ status: "wait" | "ok" | "info" | "warn";
67
+ /** The fix instructions below the line. */
68
+ details: string[];
69
+ }
70
+
71
+ /**
72
+ * The state of the service that holds the server-side skills.
73
+ *
74
+ * The vector store is the INDEX. The service holds the authoritative
75
+ * files, so it is the one that can hand out a complete skill.
76
+ */
77
+ export interface RemoteSkillStats {
78
+ /** What the service is in this run. */
79
+ state:
80
+ /** No service is configured. */
81
+ | "not-configured"
82
+ /** The service refused the call - the token or the plan. */
83
+ | "denied"
84
+ /** The service could not be reached. */
85
+ | "unavailable"
86
+ /** The service holds no skill for this user. */
87
+ | "empty"
88
+ /** The service holds skills. */
89
+ | "populated";
90
+ /** The number of skills the service holds. */
91
+ count: number;
92
+ /** Why the service could not be reached. Never a credential. */
93
+ error?: string;
94
+ }
95
+
96
+ /**
97
+ * The line before the vector store has answered.
98
+ *
99
+ * The count of the local skills is known immediately; the count of the
100
+ * vector store needs the cluster, so the line waits for it.
101
+ */
102
+ export function localSkillsReport(localCount: number): SkillsStartupReport {
103
+ return {
104
+ text: `Local Skills ${localCount} | Qdrant Skills ...`,
105
+ status: "wait",
106
+ details: [],
107
+ };
108
+ }
109
+
110
+ /**
111
+ * The line with the state of the vector store.
112
+ *
113
+ * The number is only shown when it is known: a cluster that cannot be
114
+ * reached has no count, and "0" would be a lie.
115
+ */
116
+ export function skillsReport(
117
+ localCount: number,
118
+ stats: QdrantSkillStats,
119
+ remote?: RemoteSkillStats,
120
+ ): SkillsStartupReport {
121
+ const details: string[] = [];
122
+
123
+ if (stats.schemaMismatch) {
124
+ details.push(
125
+ `The vector schema does not match: ${stats.schemaMismatch}.`,
126
+ "A collection with a different vector size cannot be searched with these vectors, so the vector search finds nothing.",
127
+ `The runtime uses the collection ${stats.collection ?? "pi-harness-skills-openai-v1"} - move the shared skills into it.`,
128
+ );
129
+ }
130
+ if (stats.state === "unavailable") {
131
+ details.push(
132
+ stats.error
133
+ ? `The vector store could not be reached: ${stats.error}`
134
+ : "The vector store could not be reached.",
135
+ );
136
+ }
137
+ if (stats.legacy) {
138
+ const legacy = stats.legacy;
139
+ details.push(
140
+ `The collection ${legacy.collection} still holds ${legacy.count} skill${legacy.count === 1 ? "" : "s"}` +
141
+ (legacy.dimensions ? ` with ${legacy.dimensions}-dimensional vectors` : "") +
142
+ (legacy.model ? ` (${legacy.model})` : "") +
143
+ " - the runtime embeds with 1536 dimensions, so these skills cannot be found until they are moved.",
144
+ `Move them into ${stats.collection ?? "pi-harness-skills-openai-v1"} - scripts/sync-to-qdrant.ts writes them there.`,
145
+ );
146
+ }
147
+ details.push(...remoteDetails(remote));
148
+
149
+ return {
150
+ text: `Local Skills ${localCount} | Qdrant Skills ${describeQdrantState(stats)}`,
151
+ status: startupStatus(stats, remote),
152
+ details,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * The lines below the count.
158
+ *
159
+ * The service is reported separately from the vector store: it holds the
160
+ * complete skills, the vector store only the index.
161
+ */
162
+ function remoteDetails(remote: RemoteSkillStats | undefined): string[] {
163
+ if (!remote || remote.state === "not-configured") return [];
164
+
165
+ if (remote.state === "populated") {
166
+ return [
167
+ `The skill service holds ${remote.count} skill${remote.count === 1 ? "" : "s"} - they can be loaded without a local copy.`,
168
+ ];
169
+ }
170
+ if (remote.state === "denied") {
171
+ return [
172
+ remote.error
173
+ ? `The skill service refused the call: ${remote.error}`
174
+ : "The skill service refused the call - the token or the plan does not allow it.",
175
+ "Fix: check SKILLS_SAAS_API_KEY and the plan of the account.",
176
+ ];
177
+ }
178
+ if (remote.state === "unavailable") {
179
+ return [
180
+ remote.error
181
+ ? `The skill service could not be reached: ${remote.error}`
182
+ : "The skill service could not be reached.",
183
+ ];
184
+ }
185
+ return [];
186
+ }
187
+
188
+ /**
189
+ * The status of the line.
190
+ *
191
+ * A vector store that is not configured is not a problem - most machines
192
+ * do not have one - so it is an information. An unreachable cluster and an
193
+ * incompatible collection are warnings: something is broken there.
194
+ */
195
+ function startupStatus(
196
+ stats: QdrantSkillStats,
197
+ remote?: RemoteSkillStats,
198
+ ): "ok" | "info" | "warn" {
199
+ if (stats.state === "unavailable" || stats.schemaMismatch) return "warn";
200
+ if (remote && (remote.state === "denied" || remote.state === "unavailable")) {
201
+ return "warn";
202
+ }
203
+ if (stats.state === "not-configured") return "info";
204
+ return "ok";
205
+ }
206
+
207
+ /**
208
+ * The short form of the vector store state.
209
+ *
210
+ * "0" means "the remote set is empty" - whether the collection does not
211
+ * exist yet or holds no skill. The other states say what is wrong.
212
+ */
213
+ function describeQdrantState(stats: QdrantSkillStats): string {
214
+ switch (stats.state) {
215
+ case "not-configured":
216
+ return "not configured";
217
+ case "unavailable":
218
+ return "unavailable";
219
+ case "missing":
220
+ case "empty":
221
+ return "0";
222
+ case "populated":
223
+ return String(stats.count);
224
+ default:
225
+ // An unknown state is treated like an unreachable cluster: no
226
+ // count is shown, because a wrong number is worse than none.
227
+ return "unavailable";
228
+ }
229
+ }
@@ -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
  }