pi-harness-runtime 1.1.394 → 1.1.396

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 +135 -4
  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 +87 -0
  29. package/packages/contracts/dist/workspace-env-format.js +174 -6
  30. package/packages/contracts/dist/workspace-env.js +120 -9
  31. package/packages/contracts/package.json +1 -1
  32. package/packages/contracts/src/workspace-env-format.ts +209 -6
  33. package/packages/contracts/src/workspace-env.ts +128 -7
  34. package/packages/contracts/test/workspace-env.test.ts +177 -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/context-manager",
3
- "version": "1.1.394",
3
+ "version": "1.1.396",
4
4
  "description": "Context Window Manager — prevents model context overflow via compaction and resume prompts (RFC-0010)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -82,6 +82,30 @@ export interface WorkspaceEnvResult {
82
82
  * without the SQLite cache.
83
83
  */
84
84
  export declare function isLauncherCommand(cmdline: string): boolean;
85
+ /**
86
+ * The RAM rule behind the "low memory" warning.
87
+ *
88
+ * The warning exists to avoid running a build AND a dev server in parallel,
89
+ * so it is about the headroom that is actually available - not about the size
90
+ * of the machine. A 15 GB laptop with 6.5 GB free can build; a 32 GB server
91
+ * with 3 GB free cannot.
92
+ *
93
+ * - Below 4 GB free: always low.
94
+ * - Below 16 GB total: a small machine needs real headroom (6 GB) before a
95
+ * parallel build and dev server are safe.
96
+ */
97
+ export declare function isLowRamWarning(totalGb: number | null, availableGb: number | null): boolean;
98
+ /**
99
+ * The headroom a build needs before it is allowed in development mode.
100
+ */
101
+ export declare const MIN_BUILD_HEADROOM_GB = 2;
102
+ /**
103
+ * The reason the build permission is (not) given.
104
+ *
105
+ * The agent has to know WHY a build is not allowed: an unknown environment
106
+ * needs an explicit request, a memory shortage needs a lighter build.
107
+ */
108
+ export declare function buildAllowanceReason(env: WorkspaceEnvResult): string;
85
109
  export type ServerState = "running" | "not-detected" | "not-checked";
86
110
  /**
87
111
  * The name of the operating system the harness runs on.
@@ -135,6 +159,14 @@ export declare const MASKED_MODE_VALUE = "<set>";
135
159
  * the name of an environment before its value may decide the mode.
136
160
  */
137
161
  export declare function isEnvShapedKey(key: string): boolean;
162
+ /**
163
+ * True for a key that belongs to the desktop session, not to the workspace.
164
+ *
165
+ * The rule is a namespace list, not a value check: the value of a session
166
+ * variable ("ubuntu", "wayland", "user") says nothing about the environment
167
+ * of a workspace, so such a variable must never name the mode.
168
+ */
169
+ export declare function isSessionVariableKey(key: string): boolean;
138
170
  /**
139
171
  * True for a key that may hold a secret.
140
172
  */
@@ -202,6 +234,61 @@ export interface ModeResolution {
202
234
  hint: DotenvVar | null;
203
235
  source: "process.env" | ".env file" | null;
204
236
  }
237
+ /**
238
+ * The deployment configuration of a repository.
239
+ *
240
+ * A repository that carries one of these files is deployed somewhere: it
241
+ * has a runtime environment of its own, so the shape of the repository
242
+ * cannot prove that the workspace is a development checkout.
243
+ */
244
+ 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"];
245
+ /**
246
+ * The facts about the repository the workspace belongs to.
247
+ *
248
+ * They are read from the file system (and from git), never from a variable:
249
+ * a published package that is deployed nowhere is source code that the
250
+ * user runs on their own machine.
251
+ */
252
+ export interface RepositoryFacts {
253
+ /** The workspace is a git checkout. */
254
+ isGitRepository: boolean;
255
+ /** The current branch ("" when it cannot be read). */
256
+ branch: string;
257
+ /** The package.json describes a package that is published to a registry. */
258
+ isPublishedPackage: boolean;
259
+ /** The repository carries a deployment configuration. */
260
+ hasDeploymentMarker: boolean;
261
+ /** A dev or build server runs in this workspace. */
262
+ hasRunningServer: boolean;
263
+ }
264
+ /**
265
+ * A mode that the facts about the repository named.
266
+ */
267
+ export interface RepositoryModeDetection {
268
+ mode: WorkspaceMode;
269
+ /**
270
+ * The phrase that names the detection.
271
+ *
272
+ * It is used as the key of the evidence ("source-only repository",
273
+ * "git branch develop"), so the label can name it the same way it
274
+ * names a variable.
275
+ */
276
+ key: string;
277
+ }
278
+ /**
279
+ * Name the mode of a workspace from the facts about its repository.
280
+ *
281
+ * This is the LAST tier of the mode detection: it runs only when no
282
+ * variable names the environment at all. It never claims production - a
283
+ * checkout of a production branch is not a production environment, and the
284
+ * deployment marker of the repository keeps the answer honest.
285
+ *
286
+ * Two sets of facts name a development workspace:
287
+ * 1. a published package without a deployment configuration and without a
288
+ * running server: source code that the user runs on their own machine
289
+ * 2. a branch that names a development line of work ("develop")
290
+ */
291
+ export declare function modeFromRepositoryFacts(facts: RepositoryFacts): RepositoryModeDetection | null;
205
292
  /**
206
293
  * Resolve the workspace mode from the variables that were found.
207
294
  *
@@ -31,6 +31,46 @@
31
31
  export function isLauncherCommand(cmdline) {
32
32
  return /^(npm|bun|yarn|pnpm)\b.*\b(run\s+)?(dev|start)\b/.test(cmdline.trim());
33
33
  }
34
+ /**
35
+ * The RAM rule behind the "low memory" warning.
36
+ *
37
+ * The warning exists to avoid running a build AND a dev server in parallel,
38
+ * so it is about the headroom that is actually available - not about the size
39
+ * of the machine. A 15 GB laptop with 6.5 GB free can build; a 32 GB server
40
+ * with 3 GB free cannot.
41
+ *
42
+ * - Below 4 GB free: always low.
43
+ * - Below 16 GB total: a small machine needs real headroom (6 GB) before a
44
+ * parallel build and dev server are safe.
45
+ */
46
+ export function isLowRamWarning(totalGb, availableGb) {
47
+ if (availableGb === null)
48
+ return false;
49
+ if (availableGb < 4)
50
+ return true;
51
+ if (totalGb === null)
52
+ return false;
53
+ return totalGb < 16 && availableGb < 6;
54
+ }
55
+ /**
56
+ * The headroom a build needs before it is allowed in development mode.
57
+ */
58
+ export const MIN_BUILD_HEADROOM_GB = 2;
59
+ /**
60
+ * The reason the build permission is (not) given.
61
+ *
62
+ * The agent has to know WHY a build is not allowed: an unknown environment
63
+ * needs an explicit request, a memory shortage needs a lighter build.
64
+ */
65
+ export function buildAllowanceReason(env) {
66
+ if (env.buildAllowed)
67
+ return "yes";
68
+ if (env.mode === "unknown")
69
+ return "no - requires explicit request";
70
+ if (env.lowRamWarning)
71
+ return "no - low memory";
72
+ return "no - requires explicit request";
73
+ }
34
74
  /**
35
75
  * The name of the operating system the harness runs on.
36
76
  *
@@ -167,6 +207,49 @@ export const MASKED_MODE_VALUE = "<set>";
167
207
  export function isEnvShapedKey(key) {
168
208
  return /(^|_)(ENV|ENVIRONMENT|MODE|STAGE)$/i.test(key);
169
209
  }
210
+ /**
211
+ * The namespaces of the desktop session variables.
212
+ *
213
+ * A session variable names the desktop session ("GNOME_SHELL_SESSION_MODE",
214
+ * "XDG_SESSION_TYPE"), never the environment of a workspace. Some of them
215
+ * end in "_MODE" or "_ENV", so without this list they look like an
216
+ * environment selector and decide the mode of a workspace that has no
217
+ * environment at all.
218
+ */
219
+ const SESSION_VARIABLE_PREFIXES = [
220
+ "GNOME_",
221
+ "XDG_",
222
+ "DESKTOP_",
223
+ "DBUS_",
224
+ "GDM_",
225
+ "SDDM_",
226
+ "LIGHTDM_",
227
+ "GTK_",
228
+ "QT_",
229
+ "MATE_",
230
+ "KDE_",
231
+ "PLASMA_",
232
+ "UNITY_",
233
+ "XFCE_",
234
+ "CINNAMON_",
235
+ "WAYLAND_",
236
+ "MIR_",
237
+ "I3_",
238
+ "TERM_",
239
+ "SSH_",
240
+ "TMUX_",
241
+ ];
242
+ /**
243
+ * True for a key that belongs to the desktop session, not to the workspace.
244
+ *
245
+ * The rule is a namespace list, not a value check: the value of a session
246
+ * variable ("ubuntu", "wayland", "user") says nothing about the environment
247
+ * of a workspace, so such a variable must never name the mode.
248
+ */
249
+ export function isSessionVariableKey(key) {
250
+ const upper = key.toUpperCase();
251
+ return SESSION_VARIABLE_PREFIXES.some((prefix) => upper.startsWith(prefix));
252
+ }
170
253
  /**
171
254
  * True for a key that may hold a secret.
172
255
  */
@@ -212,6 +295,9 @@ export function detectModeHint(vars, options = {}) {
212
295
  const nodeEnv = vars.find((v) => v.key === "NODE_ENV");
213
296
  if (nodeEnv)
214
297
  return nodeEnv;
298
+ // A session variable is not an environment selector: it must never
299
+ // become the hint that names the mode.
300
+ const usableVars = vars.filter((v) => !isSessionVariableKey(v.key));
215
301
  // The value match is opt-in: a caller that passes the process env
216
302
  // variables must not let an unrelated variable that happens to hold the
217
303
  // word "development" decide the mode.
@@ -219,7 +305,7 @@ export function detectModeHint(vars, options = {}) {
219
305
  // An environment shaped key whose value names a known mode wins: an
220
306
  // earlier "APP_MODE=staging" or an empty "APP_ENV=" of the same file
221
307
  // must not shadow the "AMOS_ENV=development" of the same file.
222
- const usable = vars.find((v) => v.key !== "NODE_ENV" &&
308
+ const usable = usableVars.find((v) => v.key !== "NODE_ENV" &&
223
309
  isEnvShapedKey(v.key) &&
224
310
  modeFromValue(v.value) !== null);
225
311
  if (usable)
@@ -227,7 +313,7 @@ export function detectModeHint(vars, options = {}) {
227
313
  }
228
314
  // No usable key: fall back to the first environment shaped key so that
229
315
  // the label can name it (it names no known mode).
230
- return vars.find((v) => v.key !== "NODE_ENV" && isEnvShapedKey(v.key)) ?? null;
316
+ return (usableVars.find((v) => v.key !== "NODE_ENV" && isEnvShapedKey(v.key)) ?? null);
231
317
  }
232
318
  /**
233
319
  * The dotenv files of a workspace, in the order they are read.
@@ -289,12 +375,71 @@ export function collectProcessEnvVars(env = process.env) {
289
375
  const value = env[key];
290
376
  if (value === undefined)
291
377
  continue;
378
+ // A session variable of the desktop is never an environment
379
+ // selector, so it is not even collected as a candidate.
380
+ if (isSessionVariableKey(key))
381
+ continue;
292
382
  if (key === "NODE_ENV" || isEnvShapedKey(key)) {
293
383
  vars.push({ key, value });
294
384
  }
295
385
  }
296
386
  return vars;
297
387
  }
388
+ /**
389
+ * The deployment configuration of a repository.
390
+ *
391
+ * A repository that carries one of these files is deployed somewhere: it
392
+ * has a runtime environment of its own, so the shape of the repository
393
+ * cannot prove that the workspace is a development checkout.
394
+ */
395
+ export const DEPLOYMENT_MARKER_FILES = [
396
+ "Dockerfile",
397
+ "Containerfile",
398
+ "docker-compose.yml",
399
+ "docker-compose.yaml",
400
+ "compose.yml",
401
+ "compose.yaml",
402
+ "Procfile",
403
+ "fly.toml",
404
+ "render.yaml",
405
+ "railway.json",
406
+ "vercel.json",
407
+ "netlify.toml",
408
+ "serverless.yml",
409
+ "serverless.yaml",
410
+ "app.yaml",
411
+ "ecosystem.config.cjs",
412
+ "ecosystem.config.js",
413
+ ];
414
+ /**
415
+ * The branches that name a development line of work.
416
+ */
417
+ const DEVELOPMENT_BRANCHES = new Set(["develop", "development"]);
418
+ /**
419
+ * Name the mode of a workspace from the facts about its repository.
420
+ *
421
+ * This is the LAST tier of the mode detection: it runs only when no
422
+ * variable names the environment at all. It never claims production - a
423
+ * checkout of a production branch is not a production environment, and the
424
+ * deployment marker of the repository keeps the answer honest.
425
+ *
426
+ * Two sets of facts name a development workspace:
427
+ * 1. a published package without a deployment configuration and without a
428
+ * running server: source code that the user runs on their own machine
429
+ * 2. a branch that names a development line of work ("develop")
430
+ */
431
+ export function modeFromRepositoryFacts(facts) {
432
+ if (!facts.isGitRepository)
433
+ return null;
434
+ if (facts.isPublishedPackage && !facts.hasDeploymentMarker && !facts.hasRunningServer) {
435
+ return { mode: "development", key: "source-only repository" };
436
+ }
437
+ const branch = facts.branch.trim().toLowerCase();
438
+ if (DEVELOPMENT_BRANCHES.has(branch)) {
439
+ return { mode: "development", key: `git branch ${branch}` };
440
+ }
441
+ return null;
442
+ }
298
443
  /**
299
444
  * Resolve the workspace mode from the variables that were found.
300
445
  *
@@ -346,6 +491,16 @@ function findModeEvidence(env) {
346
491
  // A value that names a known mode wins over a value that cannot be read.
347
492
  return candidates.find((e) => modeFromValue(e.value) !== null) ?? candidates[0];
348
493
  }
494
+ /**
495
+ * The evidence entry that names the mode through the repository (if any).
496
+ *
497
+ * A detection is weaker than a variable: it is the shape of the checkout,
498
+ * not a declaration of the environment. It is therefore only used when no
499
+ * variable names the environment at all.
500
+ */
501
+ function findDetectionEvidence(env) {
502
+ return env.evidence.find((e) => e.source === "detection");
503
+ }
349
504
  /**
350
505
  * Human readable mode label.
351
506
  *
@@ -361,9 +516,14 @@ export function describeWorkspaceMode(env) {
361
516
  const hint = findModeEvidence(env);
362
517
  const where = hint?.file ? ` in ${hint.file}` : "";
363
518
  if (env.mode !== "unknown") {
364
- return hint
365
- ? `${env.mode.toUpperCase()} (from ${hint.key}${where})`
366
- : env.mode.toUpperCase();
519
+ if (hint)
520
+ return `${env.mode.toUpperCase()} (from ${hint.key}${where})`;
521
+ // The mode came from the repository, not from a variable: the label
522
+ // names the reason ("DEVELOPMENT (source-only repository)").
523
+ const detection = findDetectionEvidence(env);
524
+ if (detection)
525
+ return `${env.mode.toUpperCase()} (${detection.key})`;
526
+ return env.mode.toUpperCase();
367
527
  }
368
528
  if (hint) {
369
529
  // The stored mode and the evidence can disagree (a stale cache row):
@@ -382,6 +542,14 @@ export function describeWorkspaceMode(env) {
382
542
  }
383
543
  return `UNKNOWN (${hint.key}=${hint.value} is not a known mode${where})`;
384
544
  }
545
+ // A stale cache row can carry a detection next to an unknown mode: the
546
+ // detection names a mode, so the mode is shown.
547
+ const detection = findDetectionEvidence(env);
548
+ if (detection) {
549
+ const named = modeFromValue(detection.value);
550
+ if (named)
551
+ return `${named.toUpperCase()} (${detection.key})`;
552
+ }
385
553
  return "UNKNOWN (no NODE_ENV or environment variable found)";
386
554
  }
387
555
  export function describeServerState(stateOrRunning, hasScript) {
@@ -481,7 +649,7 @@ export function formatEnvForAgentResult(env) {
481
649
  }
482
650
  }
483
651
  message += `\n`;
484
- message += `Build allowed: ${env.buildAllowed ? "yes" : "no - requires explicit request"}\n`;
652
+ message += `Build allowed: ${buildAllowanceReason(env)}\n`;
485
653
  if (env.mode === "unknown") {
486
654
  message += `\nNOTE: Environment unknown - a dev/build script only means a server CAN be started, it does not establish the workspace mode. Explicitly ask the user before build.\n`;
487
655
  }
@@ -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, isLowRamWarning, MASKED_MODE_VALUE, MIN_BUILD_HEADROOM_GB, 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
  /**
@@ -95,11 +95,10 @@ function detectRamInfo() {
95
95
  const freeBytes = freemem();
96
96
  const totalGb = Math.round((totalBytes / (1024 ** 3)) * 10) / 10;
97
97
  const availableGb = Math.round((freeBytes / (1024 ** 3)) * 10) / 10;
98
- const LOW_RAM_GB = 16;
99
98
  return {
100
99
  total: totalGb,
101
100
  available: availableGb,
102
- lowWarning: totalGb < LOW_RAM_GB || availableGb < 4,
101
+ lowWarning: isLowRamWarning(totalGb, availableGb),
103
102
  };
104
103
  }
105
104
  catch {
@@ -520,6 +519,97 @@ function detectPackageManager(workspacePath) {
520
519
  return "npm";
521
520
  return null;
522
521
  }
522
+ /**
523
+ * The git repository of a workspace.
524
+ *
525
+ * The root of the repository is the unit of the detection: a package
526
+ * published from a monorepo is described by the root, not by the folder
527
+ * the harness happens to run in.
528
+ */
529
+ function detectGitFacts(workspacePath) {
530
+ try {
531
+ 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 });
532
+ const lines = out.split("\n").map((line) => line.trim());
533
+ const root = lines[0] ?? "";
534
+ const branch = lines[1] ?? "";
535
+ // A detached HEAD reports "HEAD" - that is not a branch name.
536
+ return { root, branch: branch === "HEAD" ? "" : branch };
537
+ }
538
+ catch {
539
+ // No git, no repository, or a timeout: nothing is known.
540
+ return { root: "", branch: "" };
541
+ }
542
+ }
543
+ /**
544
+ * True when the package is published to a registry.
545
+ *
546
+ * A package that is published names a version and declares what it ships
547
+ * ("files", "bin" or a "publishConfig"). A repository that only holds an
548
+ * application usually declares none of them - so this stays a careful
549
+ * signal instead of "every package.json is published".
550
+ */
551
+ function isPublishedPackage(packageDir) {
552
+ const pkgJsonPath = join(packageDir, "package.json");
553
+ if (!existsSync(pkgJsonPath))
554
+ return false;
555
+ try {
556
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
557
+ if (!pkg || typeof pkg !== "object")
558
+ return false;
559
+ if (pkg.private === true)
560
+ return false;
561
+ if (typeof pkg.name !== "string" || !pkg.name)
562
+ return false;
563
+ if (typeof pkg.version !== "string" || !pkg.version)
564
+ return false;
565
+ return (Array.isArray(pkg.files) ||
566
+ (typeof pkg.bin === "object" && pkg.bin !== null) ||
567
+ (typeof pkg.publishConfig === "object" && pkg.publishConfig !== null));
568
+ }
569
+ catch {
570
+ return false;
571
+ }
572
+ }
573
+ /**
574
+ * True when the repository carries a deployment configuration.
575
+ *
576
+ * A deployment file or a deploy workflow means the repository has a
577
+ * runtime environment of its own - the shape of the repository can then no
578
+ * longer say that the workspace is a development checkout.
579
+ */
580
+ function hasDeploymentConfiguration(repoRoot) {
581
+ for (const marker of DEPLOYMENT_MARKER_FILES) {
582
+ if (existsSync(join(repoRoot, marker)))
583
+ return true;
584
+ }
585
+ const workflowDir = join(repoRoot, ".github", "workflows");
586
+ if (existsSync(workflowDir)) {
587
+ try {
588
+ for (const name of readdirSync(workflowDir)) {
589
+ if (/deploy/i.test(name))
590
+ return true;
591
+ }
592
+ }
593
+ catch {
594
+ // An unreadable directory proves nothing.
595
+ }
596
+ }
597
+ return false;
598
+ }
599
+ /**
600
+ * Read the facts about the repository the workspace belongs to.
601
+ */
602
+ function detectRepositoryFacts(workspacePath, hasRunningServer) {
603
+ const { root, branch } = detectGitFacts(workspacePath);
604
+ const repoRoot = root || workspacePath;
605
+ return {
606
+ isGitRepository: root !== "",
607
+ branch,
608
+ isPublishedPackage: isPublishedPackage(repoRoot),
609
+ hasDeploymentMarker: hasDeploymentConfiguration(repoRoot),
610
+ hasRunningServer,
611
+ };
612
+ }
523
613
  /**
524
614
  * Determine workspace mode with proper precedence
525
615
  * Never assume production - unknown requires investigation
@@ -531,8 +621,11 @@ function detectPackageManager(workspacePath) {
531
621
  * key whose value names a known mode, then any other environment shaped
532
622
  * key
533
623
  * 3. an environment shaped key of the process env (the files are silent)
624
+ * 4. the facts about the repository (only when NO variable names the
625
+ * environment at all) - a published source repository without a runtime
626
+ * of its own is a development checkout
534
627
  */
535
- function determineMode(processVars, dotenvVars, evidence) {
628
+ function determineMode(processVars, dotenvVars, evidence, repositoryFacts) {
536
629
  const resolved = resolveWorkspaceMode(processVars, dotenvVars);
537
630
  if (resolved.hint && resolved.source) {
538
631
  // The value is only kept when it is provably not a secret: the
@@ -546,8 +639,21 @@ function determineMode(processVars, dotenvVars, evidence) {
546
639
  timestamp: Date.now(),
547
640
  file: resolved.hint.file,
548
641
  });
549
- }
550
- return resolved.mode;
642
+ // An explicit variable decides - even when it names no known mode
643
+ // ("staging"). The mode stays unknown and the label names the value.
644
+ return resolved.mode;
645
+ }
646
+ // No variable names the environment: the repository may still name it.
647
+ const detection = modeFromRepositoryFacts(repositoryFacts);
648
+ if (!detection)
649
+ return resolved.mode;
650
+ evidence.push({
651
+ source: "detection",
652
+ key: detection.key,
653
+ value: detection.mode,
654
+ timestamp: Date.now(),
655
+ });
656
+ return detection.mode;
551
657
  }
552
658
  /**
553
659
  * Detect and store workspace environment
@@ -594,14 +700,19 @@ export function detectWorkspaceEnv(workspacePath) {
594
700
  // workspace (see detectServerStates).
595
701
  const { dev: devServerState, build: buildServerState } = detectServerStates(workspacePath);
596
702
  // Determine mode - NEVER assume production
597
- const mode = determineMode(processVars, dotenvVars, evidence);
703
+ const repositoryFacts = detectRepositoryFacts(workspacePath, devServerState === "running" || buildServerState === "running");
704
+ const mode = determineMode(processVars, dotenvVars, evidence, repositoryFacts);
598
705
  // Determine build permission
599
706
  let buildAllowed = false;
600
707
  if (mode === "production") {
601
708
  buildAllowed = true; // Production build is allowed
602
709
  }
603
710
  else if (mode === "development") {
604
- buildAllowed = !ramInfo.lowWarning; // Allow if not low RAM
711
+ // A build needs headroom, not a big machine. The low-memory warning is
712
+ // about running a build AND a dev server in parallel; a build on its
713
+ // own only needs enough free memory to finish.
714
+ buildAllowed =
715
+ ramInfo.available === null ? false : ramInfo.available >= MIN_BUILD_HEADROOM_GB;
605
716
  }
606
717
  else {
607
718
  buildAllowed = false; // Unknown requires investigation
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/contracts",
3
- "version": "1.1.394",
3
+ "version": "1.1.396",
4
4
  "type": "module",
5
5
  "description": "RPC schema definitions for pi-harness runtime",
6
6
  "main": "./dist/index.js",