pi-harness-runtime 1.1.394 → 1.1.395

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 (214) hide show
  1. package/AGENTS.md +125 -3
  2. package/harness/glm-quota-logger.ts +17 -0
  3. package/harness/langchain/planner-logger.ts +15 -0
  4. package/harness/service-diagnostics.ts +32 -1
  5. package/index.ts +92 -3
  6. package/package.json +1 -1
  7. package/packages/a2a-adapter/package.json +1 -1
  8. package/packages/agent-policy/package.json +1 -1
  9. package/packages/architecture-generator/package.json +1 -1
  10. package/packages/auth/package.json +1 -1
  11. package/packages/autonomous-refactor/package.json +1 -1
  12. package/packages/autonomous-runtime/package.json +1 -1
  13. package/packages/cache-strategy/package.json +1 -1
  14. package/packages/capability-registry/package.json +1 -1
  15. package/packages/checkpoint/package.json +1 -1
  16. package/packages/cli-plugin-sdk/package.json +1 -1
  17. package/packages/clipboard/package.json +1 -1
  18. package/packages/clipboard-plugin/package.json +1 -1
  19. package/packages/code-generation/package.json +1 -1
  20. package/packages/code-review/package.json +1 -1
  21. package/packages/codex-adapter/package.json +1 -1
  22. package/packages/codex-watcher/package.json +1 -1
  23. package/packages/config-capture/package.json +1 -1
  24. package/packages/connection-runtime/package.json +1 -1
  25. package/packages/context-compiler/package.json +1 -1
  26. package/packages/context-discovery/package.json +1 -1
  27. package/packages/context-manager/package.json +1 -1
  28. package/packages/contracts/dist/workspace-env-format.d.ts +63 -0
  29. package/packages/contracts/dist/workspace-env-format.js +133 -5
  30. package/packages/contracts/dist/workspace-env.js +114 -6
  31. package/packages/contracts/package.json +1 -1
  32. package/packages/contracts/src/workspace-env-format.ts +171 -5
  33. package/packages/contracts/src/workspace-env.ts +120 -4
  34. package/packages/contracts/test/workspace-env.test.ts +129 -0
  35. package/packages/cookie-sanitizer/package.json +1 -1
  36. package/packages/cost-optimizer/package.json +1 -1
  37. package/packages/dependency-analyzer/package.json +1 -1
  38. package/packages/django-plugin/package.json +1 -1
  39. package/packages/doc-generator/package.json +1 -1
  40. package/packages/durable-review/package.json +1 -1
  41. package/packages/e2e-tester/package.json +1 -1
  42. package/packages/evaluation-engine/package.json +1 -1
  43. package/packages/evaluation-runner/package.json +1 -1
  44. package/packages/event-bus/package.json +1 -1
  45. package/packages/event-sink/package.json +1 -1
  46. package/packages/event-store/package.json +1 -1
  47. package/packages/experience-replay/package.json +1 -1
  48. package/packages/feedback-collector/package.json +1 -1
  49. package/packages/file-copy-helper/package.json +1 -1
  50. package/packages/framework-detector/package.json +1 -1
  51. package/packages/framework-plugin-sdk/package.json +1 -1
  52. package/packages/frappe-plugin/package.json +1 -1
  53. package/packages/generic-web-plugin/package.json +1 -1
  54. package/packages/health-monitor/package.json +1 -1
  55. package/packages/intent-analyzer/package.json +1 -1
  56. package/packages/jev-judge/dist/continuation-controller.d.ts +47 -0
  57. package/packages/jev-judge/dist/continuation-controller.d.ts.map +1 -1
  58. package/packages/jev-judge/dist/continuation-controller.js +142 -16
  59. package/packages/jev-judge/dist/continuation-controller.js.map +1 -1
  60. package/packages/jev-judge/dist/index.d.ts +6 -0
  61. package/packages/jev-judge/dist/index.d.ts.map +1 -1
  62. package/packages/jev-judge/dist/index.js +6 -0
  63. package/packages/jev-judge/dist/index.js.map +1 -1
  64. package/packages/jev-judge/dist/validation-ledger.d.ts +129 -0
  65. package/packages/jev-judge/dist/validation-ledger.d.ts.map +1 -0
  66. package/packages/jev-judge/dist/validation-ledger.js +234 -0
  67. package/packages/jev-judge/dist/validation-ledger.js.map +1 -0
  68. package/packages/jev-judge/dist/validation-plan.d.ts +176 -0
  69. package/packages/jev-judge/dist/validation-plan.d.ts.map +1 -0
  70. package/packages/jev-judge/dist/validation-plan.js +293 -0
  71. package/packages/jev-judge/dist/validation-plan.js.map +1 -0
  72. package/packages/jev-judge/dist/validation-tie-breaker.d.ts +48 -0
  73. package/packages/jev-judge/dist/validation-tie-breaker.d.ts.map +1 -0
  74. package/packages/jev-judge/dist/validation-tie-breaker.js +83 -0
  75. package/packages/jev-judge/dist/validation-tie-breaker.js.map +1 -0
  76. package/packages/jev-judge/package.json +1 -1
  77. package/packages/jev-judge/src/continuation-controller.ts +179 -20
  78. package/packages/jev-judge/src/index.ts +41 -0
  79. package/packages/jev-judge/src/validation-ledger.ts +290 -0
  80. package/packages/jev-judge/src/validation-plan.ts +406 -0
  81. package/packages/jev-judge/src/validation-tie-breaker.ts +123 -0
  82. package/packages/jev-judge/tests/continuation-controller.test.ts +167 -0
  83. package/packages/jev-judge/tests/validation-ledger.test.ts +181 -0
  84. package/packages/jev-judge/tests/validation-plan.test.ts +170 -0
  85. package/packages/jev-judge/tests/validation-tie-breaker.test.ts +88 -0
  86. package/packages/knowledge-graph/package.json +1 -1
  87. package/packages/knowledge-retrieval/package.json +1 -1
  88. package/packages/laravel-plugin/package.json +1 -1
  89. package/packages/learning-engine/package.json +1 -1
  90. package/packages/logging/README.md +37 -0
  91. package/packages/logging/dist/buffer.d.ts +33 -0
  92. package/packages/logging/dist/buffer.d.ts.map +1 -0
  93. package/packages/logging/dist/buffer.js +60 -0
  94. package/packages/logging/dist/buffer.js.map +1 -0
  95. package/packages/logging/dist/config.d.ts +40 -0
  96. package/packages/logging/dist/config.d.ts.map +1 -0
  97. package/packages/logging/dist/config.js +114 -0
  98. package/packages/logging/dist/config.js.map +1 -0
  99. package/packages/logging/dist/index.d.ts +32 -0
  100. package/packages/logging/dist/index.d.ts.map +1 -0
  101. package/packages/logging/dist/index.js +31 -0
  102. package/packages/logging/dist/index.js.map +1 -0
  103. package/packages/logging/dist/redact.d.ts +43 -0
  104. package/packages/logging/dist/redact.d.ts.map +1 -0
  105. package/packages/logging/dist/redact.js +220 -0
  106. package/packages/logging/dist/redact.js.map +1 -0
  107. package/packages/logging/dist/sampler.d.ts +37 -0
  108. package/packages/logging/dist/sampler.d.ts.map +1 -0
  109. package/packages/logging/dist/sampler.js +78 -0
  110. package/packages/logging/dist/sampler.js.map +1 -0
  111. package/packages/logging/dist/shipper.d.ts +82 -0
  112. package/packages/logging/dist/shipper.d.ts.map +1 -0
  113. package/packages/logging/dist/shipper.js +238 -0
  114. package/packages/logging/dist/shipper.js.map +1 -0
  115. package/packages/logging/dist/singleton.d.ts +38 -0
  116. package/packages/logging/dist/singleton.d.ts.map +1 -0
  117. package/packages/logging/dist/singleton.js +95 -0
  118. package/packages/logging/dist/singleton.js.map +1 -0
  119. package/packages/logging/dist/spool.d.ts +39 -0
  120. package/packages/logging/dist/spool.d.ts.map +1 -0
  121. package/packages/logging/dist/spool.js +110 -0
  122. package/packages/logging/dist/spool.js.map +1 -0
  123. package/packages/logging/dist/transports/r2.d.ts +31 -0
  124. package/packages/logging/dist/transports/r2.d.ts.map +1 -0
  125. package/packages/logging/dist/transports/r2.js +60 -0
  126. package/packages/logging/dist/transports/r2.js.map +1 -0
  127. package/packages/logging/dist/transports/webhook.d.ts +22 -0
  128. package/packages/logging/dist/transports/webhook.d.ts.map +1 -0
  129. package/packages/logging/dist/transports/webhook.js +37 -0
  130. package/packages/logging/dist/transports/webhook.js.map +1 -0
  131. package/packages/logging/dist/types.d.ts +73 -0
  132. package/packages/logging/dist/types.d.ts.map +1 -0
  133. package/packages/logging/dist/types.js +19 -0
  134. package/packages/logging/dist/types.js.map +1 -0
  135. package/packages/logging/package.json +37 -0
  136. package/packages/logging/src/buffer.ts +77 -0
  137. package/packages/logging/src/config.ts +163 -0
  138. package/packages/logging/src/index.ts +87 -0
  139. package/packages/logging/src/redact.ts +271 -0
  140. package/packages/logging/src/sampler.ts +97 -0
  141. package/packages/logging/src/shipper.ts +281 -0
  142. package/packages/logging/src/singleton.ts +142 -0
  143. package/packages/logging/src/spool.ts +139 -0
  144. package/packages/logging/src/transports/r2.ts +86 -0
  145. package/packages/logging/src/transports/webhook.ts +54 -0
  146. package/packages/logging/src/types.ts +85 -0
  147. package/packages/logging/test/buffer.test.ts +44 -0
  148. package/packages/logging/test/config.test.ts +97 -0
  149. package/packages/logging/test/integration.test.ts +135 -0
  150. package/packages/logging/test/redact.test.ts +109 -0
  151. package/packages/logging/test/sampler.test.ts +57 -0
  152. package/packages/logging/test/shipper.test.ts +152 -0
  153. package/packages/logging/test/spool.test.ts +93 -0
  154. package/packages/mcp-adapter/package.json +1 -1
  155. package/packages/memory-engine/package.json +1 -1
  156. package/packages/milestone-manager/package.json +1 -1
  157. package/packages/model-registry/package.json +1 -1
  158. package/packages/nextjs-plugin/package.json +1 -1
  159. package/packages/notification/package.json +1 -1
  160. package/packages/observability/package.json +1 -1
  161. package/packages/okf-indexer/package.json +1 -1
  162. package/packages/paperclip-integration/package.json +1 -1
  163. package/packages/performance-optimizer/package.json +1 -1
  164. package/packages/permission-mode/package.json +1 -1
  165. package/packages/privilege-broker/package.json +1 -1
  166. package/packages/project-analyzer/package.json +1 -1
  167. package/packages/project-bootstrap/package.json +1 -1
  168. package/packages/projection-engine/package.json +1 -1
  169. package/packages/prompt-compiler/package.json +1 -1
  170. package/packages/prompt-versioning/package.json +1 -1
  171. package/packages/provider-adapter-sdk/package.json +1 -1
  172. package/packages/provider-router/package.json +1 -1
  173. package/packages/provider-selector/package.json +1 -1
  174. package/packages/providers/package.json +1 -1
  175. package/packages/qdrant-skills/package.json +1 -1
  176. package/packages/quota-manager/package.json +1 -1
  177. package/packages/rate-limiter/package.json +1 -1
  178. package/packages/react-vite-plugin/package.json +1 -1
  179. package/packages/relay/package.json +1 -1
  180. package/packages/release-manager/package.json +1 -1
  181. package/packages/requirement-compiler/package.json +1 -1
  182. package/packages/runtime/package.json +1 -1
  183. package/packages/scheduler/package.json +1 -1
  184. package/packages/scheduler-adapter/package.json +1 -1
  185. package/packages/session/package.json +1 -1
  186. package/packages/session-api/package.json +1 -1
  187. package/packages/session-export/package.json +1 -1
  188. package/packages/shared-context/package.json +1 -1
  189. package/packages/skill-mcp-client/package.json +1 -1
  190. package/packages/skill-registry/package.json +1 -1
  191. package/packages/skills/package.json +1 -1
  192. package/packages/sprint-planner/package.json +1 -1
  193. package/packages/subscription-engine/package.json +1 -1
  194. package/packages/task-compiler/package.json +1 -1
  195. package/packages/telegram/package.json +1 -1
  196. package/packages/tencentdb-memory/package.json +1 -1
  197. package/packages/tencentdb-sync/package.json +1 -1
  198. package/packages/test-data-generator/package.json +1 -1
  199. package/packages/test-generator/package.json +1 -1
  200. package/packages/todo-bd-sync/package.json +1 -1
  201. package/packages/token-estimation/package.json +1 -1
  202. package/packages/token-optimizer/package.json +1 -1
  203. package/packages/trajectory/package.json +1 -1
  204. package/packages/tui/package.json +1 -1
  205. package/packages/types/package.json +1 -1
  206. package/packages/voice-api/package.json +1 -1
  207. package/packages/webmcp-tester/package.json +1 -1
  208. package/packages/wiki/package.json +1 -1
  209. package/packages/workflow-events/package.json +1 -1
  210. package/packages/workspace-scanner/package.json +1 -1
  211. package/packages/worktree/package.json +1 -1
  212. package/packages/write-review/package.json +1 -1
  213. package/scripts/unified-webhook-server.ts +102 -0
  214. package/scripts/validation-run.ts +191 -0
@@ -135,6 +135,14 @@ export declare const MASKED_MODE_VALUE = "<set>";
135
135
  * the name of an environment before its value may decide the mode.
136
136
  */
137
137
  export declare function isEnvShapedKey(key: string): boolean;
138
+ /**
139
+ * True for a key that belongs to the desktop session, not to the workspace.
140
+ *
141
+ * The rule is a namespace list, not a value check: the value of a session
142
+ * variable ("ubuntu", "wayland", "user") says nothing about the environment
143
+ * of a workspace, so such a variable must never name the mode.
144
+ */
145
+ export declare function isSessionVariableKey(key: string): boolean;
138
146
  /**
139
147
  * True for a key that may hold a secret.
140
148
  */
@@ -202,6 +210,61 @@ export interface ModeResolution {
202
210
  hint: DotenvVar | null;
203
211
  source: "process.env" | ".env file" | null;
204
212
  }
213
+ /**
214
+ * The deployment configuration of a repository.
215
+ *
216
+ * A repository that carries one of these files is deployed somewhere: it
217
+ * has a runtime environment of its own, so the shape of the repository
218
+ * cannot prove that the workspace is a development checkout.
219
+ */
220
+ export declare const DEPLOYMENT_MARKER_FILES: readonly ["Dockerfile", "Containerfile", "docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml", "Procfile", "fly.toml", "render.yaml", "railway.json", "vercel.json", "netlify.toml", "serverless.yml", "serverless.yaml", "app.yaml", "ecosystem.config.cjs", "ecosystem.config.js"];
221
+ /**
222
+ * The facts about the repository the workspace belongs to.
223
+ *
224
+ * They are read from the file system (and from git), never from a variable:
225
+ * a published package that is deployed nowhere is source code that the
226
+ * user runs on their own machine.
227
+ */
228
+ export interface RepositoryFacts {
229
+ /** The workspace is a git checkout. */
230
+ isGitRepository: boolean;
231
+ /** The current branch ("" when it cannot be read). */
232
+ branch: string;
233
+ /** The package.json describes a package that is published to a registry. */
234
+ isPublishedPackage: boolean;
235
+ /** The repository carries a deployment configuration. */
236
+ hasDeploymentMarker: boolean;
237
+ /** A dev or build server runs in this workspace. */
238
+ hasRunningServer: boolean;
239
+ }
240
+ /**
241
+ * A mode that the facts about the repository named.
242
+ */
243
+ export interface RepositoryModeDetection {
244
+ mode: WorkspaceMode;
245
+ /**
246
+ * The phrase that names the detection.
247
+ *
248
+ * It is used as the key of the evidence ("source-only repository",
249
+ * "git branch develop"), so the label can name it the same way it
250
+ * names a variable.
251
+ */
252
+ key: string;
253
+ }
254
+ /**
255
+ * Name the mode of a workspace from the facts about its repository.
256
+ *
257
+ * This is the LAST tier of the mode detection: it runs only when no
258
+ * variable names the environment at all. It never claims production - a
259
+ * checkout of a production branch is not a production environment, and the
260
+ * deployment marker of the repository keeps the answer honest.
261
+ *
262
+ * Two sets of facts name a development workspace:
263
+ * 1. a published package without a deployment configuration and without a
264
+ * running server: source code that the user runs on their own machine
265
+ * 2. a branch that names a development line of work ("develop")
266
+ */
267
+ export declare function modeFromRepositoryFacts(facts: RepositoryFacts): RepositoryModeDetection | null;
205
268
  /**
206
269
  * Resolve the workspace mode from the variables that were found.
207
270
  *
@@ -167,6 +167,49 @@ export const MASKED_MODE_VALUE = "<set>";
167
167
  export function isEnvShapedKey(key) {
168
168
  return /(^|_)(ENV|ENVIRONMENT|MODE|STAGE)$/i.test(key);
169
169
  }
170
+ /**
171
+ * The namespaces of the desktop session variables.
172
+ *
173
+ * A session variable names the desktop session ("GNOME_SHELL_SESSION_MODE",
174
+ * "XDG_SESSION_TYPE"), never the environment of a workspace. Some of them
175
+ * end in "_MODE" or "_ENV", so without this list they look like an
176
+ * environment selector and decide the mode of a workspace that has no
177
+ * environment at all.
178
+ */
179
+ const SESSION_VARIABLE_PREFIXES = [
180
+ "GNOME_",
181
+ "XDG_",
182
+ "DESKTOP_",
183
+ "DBUS_",
184
+ "GDM_",
185
+ "SDDM_",
186
+ "LIGHTDM_",
187
+ "GTK_",
188
+ "QT_",
189
+ "MATE_",
190
+ "KDE_",
191
+ "PLASMA_",
192
+ "UNITY_",
193
+ "XFCE_",
194
+ "CINNAMON_",
195
+ "WAYLAND_",
196
+ "MIR_",
197
+ "I3_",
198
+ "TERM_",
199
+ "SSH_",
200
+ "TMUX_",
201
+ ];
202
+ /**
203
+ * True for a key that belongs to the desktop session, not to the workspace.
204
+ *
205
+ * The rule is a namespace list, not a value check: the value of a session
206
+ * variable ("ubuntu", "wayland", "user") says nothing about the environment
207
+ * of a workspace, so such a variable must never name the mode.
208
+ */
209
+ export function isSessionVariableKey(key) {
210
+ const upper = key.toUpperCase();
211
+ return SESSION_VARIABLE_PREFIXES.some((prefix) => upper.startsWith(prefix));
212
+ }
170
213
  /**
171
214
  * True for a key that may hold a secret.
172
215
  */
@@ -212,6 +255,9 @@ export function detectModeHint(vars, options = {}) {
212
255
  const nodeEnv = vars.find((v) => v.key === "NODE_ENV");
213
256
  if (nodeEnv)
214
257
  return nodeEnv;
258
+ // A session variable is not an environment selector: it must never
259
+ // become the hint that names the mode.
260
+ const usableVars = vars.filter((v) => !isSessionVariableKey(v.key));
215
261
  // The value match is opt-in: a caller that passes the process env
216
262
  // variables must not let an unrelated variable that happens to hold the
217
263
  // word "development" decide the mode.
@@ -219,7 +265,7 @@ export function detectModeHint(vars, options = {}) {
219
265
  // An environment shaped key whose value names a known mode wins: an
220
266
  // earlier "APP_MODE=staging" or an empty "APP_ENV=" of the same file
221
267
  // must not shadow the "AMOS_ENV=development" of the same file.
222
- const usable = vars.find((v) => v.key !== "NODE_ENV" &&
268
+ const usable = usableVars.find((v) => v.key !== "NODE_ENV" &&
223
269
  isEnvShapedKey(v.key) &&
224
270
  modeFromValue(v.value) !== null);
225
271
  if (usable)
@@ -227,7 +273,7 @@ export function detectModeHint(vars, options = {}) {
227
273
  }
228
274
  // No usable key: fall back to the first environment shaped key so that
229
275
  // the label can name it (it names no known mode).
230
- return vars.find((v) => v.key !== "NODE_ENV" && isEnvShapedKey(v.key)) ?? null;
276
+ return (usableVars.find((v) => v.key !== "NODE_ENV" && isEnvShapedKey(v.key)) ?? null);
231
277
  }
232
278
  /**
233
279
  * The dotenv files of a workspace, in the order they are read.
@@ -289,12 +335,71 @@ export function collectProcessEnvVars(env = process.env) {
289
335
  const value = env[key];
290
336
  if (value === undefined)
291
337
  continue;
338
+ // A session variable of the desktop is never an environment
339
+ // selector, so it is not even collected as a candidate.
340
+ if (isSessionVariableKey(key))
341
+ continue;
292
342
  if (key === "NODE_ENV" || isEnvShapedKey(key)) {
293
343
  vars.push({ key, value });
294
344
  }
295
345
  }
296
346
  return vars;
297
347
  }
348
+ /**
349
+ * The deployment configuration of a repository.
350
+ *
351
+ * A repository that carries one of these files is deployed somewhere: it
352
+ * has a runtime environment of its own, so the shape of the repository
353
+ * cannot prove that the workspace is a development checkout.
354
+ */
355
+ export const DEPLOYMENT_MARKER_FILES = [
356
+ "Dockerfile",
357
+ "Containerfile",
358
+ "docker-compose.yml",
359
+ "docker-compose.yaml",
360
+ "compose.yml",
361
+ "compose.yaml",
362
+ "Procfile",
363
+ "fly.toml",
364
+ "render.yaml",
365
+ "railway.json",
366
+ "vercel.json",
367
+ "netlify.toml",
368
+ "serverless.yml",
369
+ "serverless.yaml",
370
+ "app.yaml",
371
+ "ecosystem.config.cjs",
372
+ "ecosystem.config.js",
373
+ ];
374
+ /**
375
+ * The branches that name a development line of work.
376
+ */
377
+ const DEVELOPMENT_BRANCHES = new Set(["develop", "development"]);
378
+ /**
379
+ * Name the mode of a workspace from the facts about its repository.
380
+ *
381
+ * This is the LAST tier of the mode detection: it runs only when no
382
+ * variable names the environment at all. It never claims production - a
383
+ * checkout of a production branch is not a production environment, and the
384
+ * deployment marker of the repository keeps the answer honest.
385
+ *
386
+ * Two sets of facts name a development workspace:
387
+ * 1. a published package without a deployment configuration and without a
388
+ * running server: source code that the user runs on their own machine
389
+ * 2. a branch that names a development line of work ("develop")
390
+ */
391
+ export function modeFromRepositoryFacts(facts) {
392
+ if (!facts.isGitRepository)
393
+ return null;
394
+ if (facts.isPublishedPackage && !facts.hasDeploymentMarker && !facts.hasRunningServer) {
395
+ return { mode: "development", key: "source-only repository" };
396
+ }
397
+ const branch = facts.branch.trim().toLowerCase();
398
+ if (DEVELOPMENT_BRANCHES.has(branch)) {
399
+ return { mode: "development", key: `git branch ${branch}` };
400
+ }
401
+ return null;
402
+ }
298
403
  /**
299
404
  * Resolve the workspace mode from the variables that were found.
300
405
  *
@@ -346,6 +451,16 @@ function findModeEvidence(env) {
346
451
  // A value that names a known mode wins over a value that cannot be read.
347
452
  return candidates.find((e) => modeFromValue(e.value) !== null) ?? candidates[0];
348
453
  }
454
+ /**
455
+ * The evidence entry that names the mode through the repository (if any).
456
+ *
457
+ * A detection is weaker than a variable: it is the shape of the checkout,
458
+ * not a declaration of the environment. It is therefore only used when no
459
+ * variable names the environment at all.
460
+ */
461
+ function findDetectionEvidence(env) {
462
+ return env.evidence.find((e) => e.source === "detection");
463
+ }
349
464
  /**
350
465
  * Human readable mode label.
351
466
  *
@@ -361,9 +476,14 @@ export function describeWorkspaceMode(env) {
361
476
  const hint = findModeEvidence(env);
362
477
  const where = hint?.file ? ` in ${hint.file}` : "";
363
478
  if (env.mode !== "unknown") {
364
- return hint
365
- ? `${env.mode.toUpperCase()} (from ${hint.key}${where})`
366
- : env.mode.toUpperCase();
479
+ if (hint)
480
+ return `${env.mode.toUpperCase()} (from ${hint.key}${where})`;
481
+ // The mode came from the repository, not from a variable: the label
482
+ // names the reason ("DEVELOPMENT (source-only repository)").
483
+ const detection = findDetectionEvidence(env);
484
+ if (detection)
485
+ return `${env.mode.toUpperCase()} (${detection.key})`;
486
+ return env.mode.toUpperCase();
367
487
  }
368
488
  if (hint) {
369
489
  // The stored mode and the evidence can disagree (a stale cache row):
@@ -382,6 +502,14 @@ export function describeWorkspaceMode(env) {
382
502
  }
383
503
  return `UNKNOWN (${hint.key}=${hint.value} is not a known mode${where})`;
384
504
  }
505
+ // A stale cache row can carry a detection next to an unknown mode: the
506
+ // detection names a mode, so the mode is shown.
507
+ const detection = findDetectionEvidence(env);
508
+ if (detection) {
509
+ const named = modeFromValue(detection.value);
510
+ if (named)
511
+ return `${named.toUpperCase()} (${detection.key})`;
512
+ }
385
513
  return "UNKNOWN (no NODE_ENV or environment variable found)";
386
514
  }
387
515
  export function describeServerState(stateOrRunning, hasScript) {
@@ -13,10 +13,10 @@
13
13
  */
14
14
  import { DatabaseSync } from "node:sqlite";
15
15
  import { execSync } from "child_process";
16
- import { existsSync, readFileSync, mkdirSync, readlinkSync, realpathSync } from "fs";
16
+ import { existsSync, readFileSync, readdirSync, mkdirSync, readlinkSync, realpathSync } from "fs";
17
17
  import { join } from "path";
18
18
  import { platform, totalmem, freemem } from "os";
19
- import { canShowModeValue, collectDotenvVars, collectProcessEnvVars, describeCwdCommand, describeOs, describeServerState, describeWorkspaceMode, detectModeHint, filterOwnPids, formatEnvForAgentResult, formatEnvStatusLine as formatStatusLine, formatWorkspacePath, isLauncherCommand, MASKED_MODE_VALUE, modeFromValue, orderDotenvFileNames, parseDotenvVars, resolveWorkspaceMode, } from "./workspace-env-format.js";
19
+ import { canShowModeValue, collectDotenvVars, collectProcessEnvVars, describeCwdCommand, describeOs, describeServerState, describeWorkspaceMode, detectModeHint, DEPLOYMENT_MARKER_FILES, filterOwnPids, formatEnvForAgentResult, formatEnvStatusLine as formatStatusLine, formatWorkspacePath, isLauncherCommand, MASKED_MODE_VALUE, modeFromRepositoryFacts, modeFromValue, orderDotenvFileNames, parseDotenvVars, resolveWorkspaceMode, } from "./workspace-env-format.js";
20
20
  import { readDotenvFiles } from "./workspace-dotenv.js";
21
21
  const STORE_DIR = join(process.env.HOME || "/home/frappe", ".pi-harness-runtime");
22
22
  /**
@@ -520,6 +520,97 @@ function detectPackageManager(workspacePath) {
520
520
  return "npm";
521
521
  return null;
522
522
  }
523
+ /**
524
+ * The git repository of a workspace.
525
+ *
526
+ * The root of the repository is the unit of the detection: a package
527
+ * published from a monorepo is described by the root, not by the folder
528
+ * the harness happens to run in.
529
+ */
530
+ function detectGitFacts(workspacePath) {
531
+ try {
532
+ const out = execSync("git rev-parse --show-toplevel 2>/dev/null; git rev-parse --abbrev-ref HEAD 2>/dev/null", { cwd: workspacePath, encoding: "utf8", timeout: 1500 });
533
+ const lines = out.split("\n").map((line) => line.trim());
534
+ const root = lines[0] ?? "";
535
+ const branch = lines[1] ?? "";
536
+ // A detached HEAD reports "HEAD" - that is not a branch name.
537
+ return { root, branch: branch === "HEAD" ? "" : branch };
538
+ }
539
+ catch {
540
+ // No git, no repository, or a timeout: nothing is known.
541
+ return { root: "", branch: "" };
542
+ }
543
+ }
544
+ /**
545
+ * True when the package is published to a registry.
546
+ *
547
+ * A package that is published names a version and declares what it ships
548
+ * ("files", "bin" or a "publishConfig"). A repository that only holds an
549
+ * application usually declares none of them - so this stays a careful
550
+ * signal instead of "every package.json is published".
551
+ */
552
+ function isPublishedPackage(packageDir) {
553
+ const pkgJsonPath = join(packageDir, "package.json");
554
+ if (!existsSync(pkgJsonPath))
555
+ return false;
556
+ try {
557
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
558
+ if (!pkg || typeof pkg !== "object")
559
+ return false;
560
+ if (pkg.private === true)
561
+ return false;
562
+ if (typeof pkg.name !== "string" || !pkg.name)
563
+ return false;
564
+ if (typeof pkg.version !== "string" || !pkg.version)
565
+ return false;
566
+ return (Array.isArray(pkg.files) ||
567
+ (typeof pkg.bin === "object" && pkg.bin !== null) ||
568
+ (typeof pkg.publishConfig === "object" && pkg.publishConfig !== null));
569
+ }
570
+ catch {
571
+ return false;
572
+ }
573
+ }
574
+ /**
575
+ * True when the repository carries a deployment configuration.
576
+ *
577
+ * A deployment file or a deploy workflow means the repository has a
578
+ * runtime environment of its own - the shape of the repository can then no
579
+ * longer say that the workspace is a development checkout.
580
+ */
581
+ function hasDeploymentConfiguration(repoRoot) {
582
+ for (const marker of DEPLOYMENT_MARKER_FILES) {
583
+ if (existsSync(join(repoRoot, marker)))
584
+ return true;
585
+ }
586
+ const workflowDir = join(repoRoot, ".github", "workflows");
587
+ if (existsSync(workflowDir)) {
588
+ try {
589
+ for (const name of readdirSync(workflowDir)) {
590
+ if (/deploy/i.test(name))
591
+ return true;
592
+ }
593
+ }
594
+ catch {
595
+ // An unreadable directory proves nothing.
596
+ }
597
+ }
598
+ return false;
599
+ }
600
+ /**
601
+ * Read the facts about the repository the workspace belongs to.
602
+ */
603
+ function detectRepositoryFacts(workspacePath, hasRunningServer) {
604
+ const { root, branch } = detectGitFacts(workspacePath);
605
+ const repoRoot = root || workspacePath;
606
+ return {
607
+ isGitRepository: root !== "",
608
+ branch,
609
+ isPublishedPackage: isPublishedPackage(repoRoot),
610
+ hasDeploymentMarker: hasDeploymentConfiguration(repoRoot),
611
+ hasRunningServer,
612
+ };
613
+ }
523
614
  /**
524
615
  * Determine workspace mode with proper precedence
525
616
  * Never assume production - unknown requires investigation
@@ -531,8 +622,11 @@ function detectPackageManager(workspacePath) {
531
622
  * key whose value names a known mode, then any other environment shaped
532
623
  * key
533
624
  * 3. an environment shaped key of the process env (the files are silent)
625
+ * 4. the facts about the repository (only when NO variable names the
626
+ * environment at all) - a published source repository without a runtime
627
+ * of its own is a development checkout
534
628
  */
535
- function determineMode(processVars, dotenvVars, evidence) {
629
+ function determineMode(processVars, dotenvVars, evidence, repositoryFacts) {
536
630
  const resolved = resolveWorkspaceMode(processVars, dotenvVars);
537
631
  if (resolved.hint && resolved.source) {
538
632
  // The value is only kept when it is provably not a secret: the
@@ -546,8 +640,21 @@ function determineMode(processVars, dotenvVars, evidence) {
546
640
  timestamp: Date.now(),
547
641
  file: resolved.hint.file,
548
642
  });
549
- }
550
- return resolved.mode;
643
+ // An explicit variable decides - even when it names no known mode
644
+ // ("staging"). The mode stays unknown and the label names the value.
645
+ return resolved.mode;
646
+ }
647
+ // No variable names the environment: the repository may still name it.
648
+ const detection = modeFromRepositoryFacts(repositoryFacts);
649
+ if (!detection)
650
+ return resolved.mode;
651
+ evidence.push({
652
+ source: "detection",
653
+ key: detection.key,
654
+ value: detection.mode,
655
+ timestamp: Date.now(),
656
+ });
657
+ return detection.mode;
551
658
  }
552
659
  /**
553
660
  * Detect and store workspace environment
@@ -594,7 +701,8 @@ export function detectWorkspaceEnv(workspacePath) {
594
701
  // workspace (see detectServerStates).
595
702
  const { dev: devServerState, build: buildServerState } = detectServerStates(workspacePath);
596
703
  // Determine mode - NEVER assume production
597
- const mode = determineMode(processVars, dotenvVars, evidence);
704
+ const repositoryFacts = detectRepositoryFacts(workspacePath, devServerState === "running" || buildServerState === "running");
705
+ const mode = determineMode(processVars, dotenvVars, evidence, repositoryFacts);
598
706
  // Determine build permission
599
707
  let buildAllowed = false;
600
708
  if (mode === "production") {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/contracts",
3
- "version": "1.1.394",
3
+ "version": "1.1.395",
4
4
  "type": "module",
5
5
  "description": "RPC schema definitions for pi-harness runtime",
6
6
  "main": "./dist/index.js",
@@ -231,6 +231,51 @@ export function isEnvShapedKey(key: string): boolean {
231
231
  return /(^|_)(ENV|ENVIRONMENT|MODE|STAGE)$/i.test(key);
232
232
  }
233
233
 
234
+ /**
235
+ * The namespaces of the desktop session variables.
236
+ *
237
+ * A session variable names the desktop session ("GNOME_SHELL_SESSION_MODE",
238
+ * "XDG_SESSION_TYPE"), never the environment of a workspace. Some of them
239
+ * end in "_MODE" or "_ENV", so without this list they look like an
240
+ * environment selector and decide the mode of a workspace that has no
241
+ * environment at all.
242
+ */
243
+ const SESSION_VARIABLE_PREFIXES = [
244
+ "GNOME_",
245
+ "XDG_",
246
+ "DESKTOP_",
247
+ "DBUS_",
248
+ "GDM_",
249
+ "SDDM_",
250
+ "LIGHTDM_",
251
+ "GTK_",
252
+ "QT_",
253
+ "MATE_",
254
+ "KDE_",
255
+ "PLASMA_",
256
+ "UNITY_",
257
+ "XFCE_",
258
+ "CINNAMON_",
259
+ "WAYLAND_",
260
+ "MIR_",
261
+ "I3_",
262
+ "TERM_",
263
+ "SSH_",
264
+ "TMUX_",
265
+ ];
266
+
267
+ /**
268
+ * True for a key that belongs to the desktop session, not to the workspace.
269
+ *
270
+ * The rule is a namespace list, not a value check: the value of a session
271
+ * variable ("ubuntu", "wayland", "user") says nothing about the environment
272
+ * of a workspace, so such a variable must never name the mode.
273
+ */
274
+ export function isSessionVariableKey(key: string): boolean {
275
+ const upper = key.toUpperCase();
276
+ return SESSION_VARIABLE_PREFIXES.some((prefix) => upper.startsWith(prefix));
277
+ }
278
+
234
279
  /**
235
280
  * True for a key that may hold a secret.
236
281
  */
@@ -282,6 +327,9 @@ export function detectModeHint(
282
327
  ): DotenvVar | null {
283
328
  const nodeEnv = vars.find((v) => v.key === "NODE_ENV");
284
329
  if (nodeEnv) return nodeEnv;
330
+ // A session variable is not an environment selector: it must never
331
+ // become the hint that names the mode.
332
+ const usableVars = vars.filter((v) => !isSessionVariableKey(v.key));
285
333
  // The value match is opt-in: a caller that passes the process env
286
334
  // variables must not let an unrelated variable that happens to hold the
287
335
  // word "development" decide the mode.
@@ -289,7 +337,7 @@ export function detectModeHint(
289
337
  // An environment shaped key whose value names a known mode wins: an
290
338
  // earlier "APP_MODE=staging" or an empty "APP_ENV=" of the same file
291
339
  // must not shadow the "AMOS_ENV=development" of the same file.
292
- const usable = vars.find(
340
+ const usable = usableVars.find(
293
341
  (v) =>
294
342
  v.key !== "NODE_ENV" &&
295
343
  isEnvShapedKey(v.key) &&
@@ -299,7 +347,9 @@ export function detectModeHint(
299
347
  }
300
348
  // No usable key: fall back to the first environment shaped key so that
301
349
  // the label can name it (it names no known mode).
302
- return vars.find((v) => v.key !== "NODE_ENV" && isEnvShapedKey(v.key)) ?? null;
350
+ return (
351
+ usableVars.find((v) => v.key !== "NODE_ENV" && isEnvShapedKey(v.key)) ?? null
352
+ );
303
353
  }
304
354
 
305
355
  /**
@@ -363,6 +413,9 @@ export function collectProcessEnvVars(env: NodeJS.ProcessEnv = process.env): Dot
363
413
  for (const key of Object.keys(env)) {
364
414
  const value = env[key];
365
415
  if (value === undefined) continue;
416
+ // A session variable of the desktop is never an environment
417
+ // selector, so it is not even collected as a candidate.
418
+ if (isSessionVariableKey(key)) continue;
366
419
  if (key === "NODE_ENV" || isEnvShapedKey(key)) {
367
420
  vars.push({ key, value });
368
421
  }
@@ -379,6 +432,98 @@ export interface ModeResolution {
379
432
  source: "process.env" | ".env file" | null;
380
433
  }
381
434
 
435
+ /**
436
+ * The deployment configuration of a repository.
437
+ *
438
+ * A repository that carries one of these files is deployed somewhere: it
439
+ * has a runtime environment of its own, so the shape of the repository
440
+ * cannot prove that the workspace is a development checkout.
441
+ */
442
+ export const DEPLOYMENT_MARKER_FILES = [
443
+ "Dockerfile",
444
+ "Containerfile",
445
+ "docker-compose.yml",
446
+ "docker-compose.yaml",
447
+ "compose.yml",
448
+ "compose.yaml",
449
+ "Procfile",
450
+ "fly.toml",
451
+ "render.yaml",
452
+ "railway.json",
453
+ "vercel.json",
454
+ "netlify.toml",
455
+ "serverless.yml",
456
+ "serverless.yaml",
457
+ "app.yaml",
458
+ "ecosystem.config.cjs",
459
+ "ecosystem.config.js",
460
+ ] as const;
461
+
462
+ /**
463
+ * The facts about the repository the workspace belongs to.
464
+ *
465
+ * They are read from the file system (and from git), never from a variable:
466
+ * a published package that is deployed nowhere is source code that the
467
+ * user runs on their own machine.
468
+ */
469
+ export interface RepositoryFacts {
470
+ /** The workspace is a git checkout. */
471
+ isGitRepository: boolean;
472
+ /** The current branch ("" when it cannot be read). */
473
+ branch: string;
474
+ /** The package.json describes a package that is published to a registry. */
475
+ isPublishedPackage: boolean;
476
+ /** The repository carries a deployment configuration. */
477
+ hasDeploymentMarker: boolean;
478
+ /** A dev or build server runs in this workspace. */
479
+ hasRunningServer: boolean;
480
+ }
481
+
482
+ /**
483
+ * A mode that the facts about the repository named.
484
+ */
485
+ export interface RepositoryModeDetection {
486
+ mode: WorkspaceMode;
487
+ /**
488
+ * The phrase that names the detection.
489
+ *
490
+ * It is used as the key of the evidence ("source-only repository",
491
+ * "git branch develop"), so the label can name it the same way it
492
+ * names a variable.
493
+ */
494
+ key: string;
495
+ }
496
+
497
+ /**
498
+ * The branches that name a development line of work.
499
+ */
500
+ const DEVELOPMENT_BRANCHES = new Set(["develop", "development"]);
501
+
502
+ /**
503
+ * Name the mode of a workspace from the facts about its repository.
504
+ *
505
+ * This is the LAST tier of the mode detection: it runs only when no
506
+ * variable names the environment at all. It never claims production - a
507
+ * checkout of a production branch is not a production environment, and the
508
+ * deployment marker of the repository keeps the answer honest.
509
+ *
510
+ * Two sets of facts name a development workspace:
511
+ * 1. a published package without a deployment configuration and without a
512
+ * running server: source code that the user runs on their own machine
513
+ * 2. a branch that names a development line of work ("develop")
514
+ */
515
+ export function modeFromRepositoryFacts(facts: RepositoryFacts): RepositoryModeDetection | null {
516
+ if (!facts.isGitRepository) return null;
517
+ if (facts.isPublishedPackage && !facts.hasDeploymentMarker && !facts.hasRunningServer) {
518
+ return { mode: "development", key: "source-only repository" };
519
+ }
520
+ const branch = facts.branch.trim().toLowerCase();
521
+ if (DEVELOPMENT_BRANCHES.has(branch)) {
522
+ return { mode: "development", key: `git branch ${branch}` };
523
+ }
524
+ return null;
525
+ }
526
+
382
527
  /**
383
528
  * Resolve the workspace mode from the variables that were found.
384
529
  *
@@ -436,6 +581,17 @@ function findModeEvidence(env: WorkspaceEnvResult): EnvEvidence | undefined {
436
581
  return candidates.find((e) => modeFromValue(e.value) !== null) ?? candidates[0];
437
582
  }
438
583
 
584
+ /**
585
+ * The evidence entry that names the mode through the repository (if any).
586
+ *
587
+ * A detection is weaker than a variable: it is the shape of the checkout,
588
+ * not a declaration of the environment. It is therefore only used when no
589
+ * variable names the environment at all.
590
+ */
591
+ function findDetectionEvidence(env: WorkspaceEnvResult): EnvEvidence | undefined {
592
+ return env.evidence.find((e) => e.source === "detection");
593
+ }
594
+
439
595
  /**
440
596
  * Human readable mode label.
441
597
  *
@@ -451,9 +607,12 @@ export function describeWorkspaceMode(env: WorkspaceEnvResult): string {
451
607
  const hint = findModeEvidence(env);
452
608
  const where = hint?.file ? ` in ${hint.file}` : "";
453
609
  if (env.mode !== "unknown") {
454
- return hint
455
- ? `${env.mode.toUpperCase()} (from ${hint.key}${where})`
456
- : env.mode.toUpperCase();
610
+ if (hint) return `${env.mode.toUpperCase()} (from ${hint.key}${where})`;
611
+ // The mode came from the repository, not from a variable: the label
612
+ // names the reason ("DEVELOPMENT (source-only repository)").
613
+ const detection = findDetectionEvidence(env);
614
+ if (detection) return `${env.mode.toUpperCase()} (${detection.key})`;
615
+ return env.mode.toUpperCase();
457
616
  }
458
617
  if (hint) {
459
618
  // The stored mode and the evidence can disagree (a stale cache row):
@@ -470,6 +629,13 @@ export function describeWorkspaceMode(env: WorkspaceEnvResult): string {
470
629
  }
471
630
  return `UNKNOWN (${hint.key}=${hint.value} is not a known mode${where})`;
472
631
  }
632
+ // A stale cache row can carry a detection next to an unknown mode: the
633
+ // detection names a mode, so the mode is shown.
634
+ const detection = findDetectionEvidence(env);
635
+ if (detection) {
636
+ const named = modeFromValue(detection.value);
637
+ if (named) return `${named.toUpperCase()} (${detection.key})`;
638
+ }
473
639
  return "UNKNOWN (no NODE_ENV or environment variable found)";
474
640
  }
475
641