pi-harness-runtime 1.1.367 → 1.1.369

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 (116) hide show
  1. package/AGENTS.md +33 -0
  2. package/package.json +1 -1
  3. package/packages/a2a-adapter/package.json +1 -1
  4. package/packages/agent-policy/dist/extension.d.ts.map +1 -1
  5. package/packages/agent-policy/dist/extension.js +15 -2
  6. package/packages/agent-policy/dist/extension.js.map +1 -1
  7. package/packages/agent-policy/package.json +1 -1
  8. package/packages/agent-policy/src/extension.ts +15 -2
  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.d.ts +35 -14
  29. package/packages/contracts/dist/workspace-env.js +333 -132
  30. package/packages/contracts/package.json +1 -1
  31. package/packages/contracts/src/workspace-env.ts +411 -157
  32. package/packages/cookie-sanitizer/package.json +1 -1
  33. package/packages/cost-optimizer/package.json +1 -1
  34. package/packages/dependency-analyzer/package.json +1 -1
  35. package/packages/django-plugin/package.json +1 -1
  36. package/packages/doc-generator/package.json +1 -1
  37. package/packages/durable-review/package.json +1 -1
  38. package/packages/e2e-tester/package.json +1 -1
  39. package/packages/evaluation-engine/package.json +1 -1
  40. package/packages/evaluation-runner/package.json +1 -1
  41. package/packages/event-bus/package.json +1 -1
  42. package/packages/event-sink/package.json +1 -1
  43. package/packages/event-store/package.json +1 -1
  44. package/packages/experience-replay/package.json +1 -1
  45. package/packages/feedback-collector/package.json +1 -1
  46. package/packages/file-copy-helper/package.json +1 -1
  47. package/packages/framework-detector/package.json +1 -1
  48. package/packages/framework-plugin-sdk/package.json +1 -1
  49. package/packages/frappe-plugin/package.json +1 -1
  50. package/packages/generic-web-plugin/package.json +1 -1
  51. package/packages/health-monitor/package.json +1 -1
  52. package/packages/intent-analyzer/package.json +1 -1
  53. package/packages/jev-judge/package.json +1 -1
  54. package/packages/knowledge-graph/package.json +1 -1
  55. package/packages/knowledge-retrieval/package.json +1 -1
  56. package/packages/laravel-plugin/package.json +1 -1
  57. package/packages/learning-engine/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/package.json +1 -1
  80. package/packages/quota-manager/package.json +1 -1
  81. package/packages/rate-limiter/package.json +1 -1
  82. package/packages/react-vite-plugin/package.json +1 -1
  83. package/packages/relay/package.json +1 -1
  84. package/packages/release-manager/package.json +1 -1
  85. package/packages/requirement-compiler/package.json +1 -1
  86. package/packages/runtime/package.json +1 -1
  87. package/packages/scheduler/package.json +1 -1
  88. package/packages/scheduler-adapter/package.json +1 -1
  89. package/packages/session/package.json +1 -1
  90. package/packages/session-api/package.json +1 -1
  91. package/packages/session-export/package.json +1 -1
  92. package/packages/shared-context/package.json +1 -1
  93. package/packages/skill-mcp-client/package.json +1 -1
  94. package/packages/skill-registry/package.json +1 -1
  95. package/packages/skills/package.json +1 -1
  96. package/packages/sprint-planner/package.json +1 -1
  97. package/packages/subscription-engine/package.json +1 -1
  98. package/packages/task-compiler/package.json +1 -1
  99. package/packages/telegram/package.json +1 -1
  100. package/packages/tencentdb-memory/package.json +1 -1
  101. package/packages/tencentdb-sync/package.json +1 -1
  102. package/packages/test-data-generator/package.json +1 -1
  103. package/packages/test-generator/package.json +1 -1
  104. package/packages/todo-bd-sync/package.json +1 -1
  105. package/packages/token-estimation/package.json +1 -1
  106. package/packages/token-optimizer/package.json +1 -1
  107. package/packages/trajectory/package.json +1 -1
  108. package/packages/tui/package.json +1 -1
  109. package/packages/types/package.json +1 -1
  110. package/packages/voice-api/package.json +1 -1
  111. package/packages/webmcp-tester/package.json +1 -1
  112. package/packages/wiki/package.json +1 -1
  113. package/packages/workflow-events/package.json +1 -1
  114. package/packages/workspace-scanner/package.json +1 -1
  115. package/packages/worktree/package.json +1 -1
  116. package/packages/write-review/package.json +1 -1
@@ -2,28 +2,27 @@
2
2
  * Workspace Environment Detector
3
3
  *
4
4
  * Detects if the workspace is a development environment by checking:
5
- * 1. .env file existence and NODE_ENV value
6
- * 2. package.json "dev" script presence
7
- * 3. .env.development or .env.local files
5
+ * 1. process.env.NODE_ENV (for build processes)
6
+ * 2. .env file existence and NODE_ENV value
7
+ * 3. package.json "dev" script presence
8
8
  *
9
9
  * Stores detected environment in SQLite for agent reference.
10
+ * Uses synchronous directory creation and exception-safe operations.
10
11
  */
11
12
 
12
13
  import { DatabaseSync } from "node:sqlite";
13
- import { existsSync, readFileSync } from "fs";
14
- import { join, dirname } from "path";
15
- import { fileURLToPath } from "url";
14
+ import { existsSync, readFileSync, mkdirSync } from "fs";
15
+ import { join } from "path";
16
+ import { platform, totalmem, freemem } from "os";
16
17
 
17
- const __dirname = dirname(fileURLToPath(import.meta.url));
18
18
  const STORE_DIR = join(process.env.HOME || "/home/frappe", ".pi-harness-runtime");
19
19
 
20
20
  /**
21
- * Ensure store directory exists
21
+ * Ensure store directory exists synchronously
22
22
  */
23
23
  function ensureStoreDir(): void {
24
24
  if (!existsSync(STORE_DIR)) {
25
- // mkdirSync is not available in all contexts, use fs promises or exec
26
- import("fs").then((fs) => fs.mkdirSync(STORE_DIR, { recursive: true }));
25
+ mkdirSync(STORE_DIR, { recursive: true });
27
26
  }
28
27
  }
29
28
 
@@ -39,15 +38,23 @@ function getDbPath(): string {
39
38
  * Initialize workspace environment database
40
39
  */
41
40
  function initDatabase(): DatabaseSync {
41
+ ensureStoreDir();
42
42
  const db = new DatabaseSync(getDbPath());
43
43
  db.exec(`
44
44
  CREATE TABLE IF NOT EXISTS workspace_env (
45
45
  workspace_path TEXT PRIMARY KEY,
46
- is_dev_env INTEGER NOT NULL DEFAULT 0,
46
+ mode TEXT NOT NULL DEFAULT 'unknown',
47
+ evidence TEXT NOT NULL DEFAULT '[]',
47
48
  has_dev_script INTEGER NOT NULL DEFAULT 0,
48
- node_env TEXT,
49
- has_env_file INTEGER NOT NULL DEFAULT 0,
50
- ram_gb REAL,
49
+ has_build_script INTEGER NOT NULL DEFAULT 0,
50
+ package_manager TEXT,
51
+ dev_server_running INTEGER NOT NULL DEFAULT 0,
52
+ build_server_running INTEGER NOT NULL DEFAULT 0,
53
+ ram_total_gb REAL,
54
+ ram_available_gb REAL,
55
+ ram_effective_limit_gb REAL,
56
+ low_ram_warning INTEGER NOT NULL DEFAULT 0,
57
+ build_allowed INTEGER NOT NULL DEFAULT -1,
51
58
  detected_at INTEGER NOT NULL,
52
59
  updated_at INTEGER NOT NULL
53
60
  )
@@ -56,40 +63,181 @@ function initDatabase(): DatabaseSync {
56
63
  }
57
64
 
58
65
  /**
59
- * Detect RAM in GB
66
+ * Workspace mode with explicit states
67
+ */
68
+ export type WorkspaceMode = "development" | "production" | "test" | "unknown";
69
+
70
+ /**
71
+ * Evidence source for mode detection
72
+ */
73
+ export interface EnvEvidence {
74
+ source: "process.env" | ".env file" | "package.json" | "command" | "detection";
75
+ key: string;
76
+ value: string;
77
+ timestamp: number;
78
+ }
79
+
80
+ /**
81
+ * Workspace environment result
60
82
  */
61
- function detectRamGb(): number | null {
83
+ export interface WorkspaceEnvResult {
84
+ workspacePath: string;
85
+ mode: WorkspaceMode;
86
+ evidence: EnvEvidence[];
87
+ hasDevScript: boolean;
88
+ hasBuildScript: boolean;
89
+ packageManager: string | null;
90
+ devServerRunning: boolean;
91
+ buildServerRunning: boolean;
92
+ ramTotalGb: number | null;
93
+ ramAvailableGb: number | null;
94
+ ramEffectiveLimitGb: number | null;
95
+ lowRamWarning: boolean;
96
+ buildAllowed: boolean;
97
+ detectedAt: number;
98
+ }
99
+
100
+ /**
101
+ * Parse dotenv file without loading secrets
102
+ * Returns only NODE_ENV value, ignoring comments and other vars
103
+ */
104
+ function parseNodeEnvFromDotenv(content: string): string | null {
105
+ const lines = content.split("\n");
106
+ for (const line of lines) {
107
+ // Skip empty lines and comments
108
+ const trimmed = line.trim();
109
+ if (!trimmed || trimmed.startsWith("#")) continue;
110
+
111
+ // Match NODE_ENV= followed by value (handle quotes, spaces)
112
+ // Allow: NODE_ENV=development, NODE_ENV="production", NODE_ENV: development
113
+ const match = trimmed.match(/^NODE_ENV\s*[=:]\s*['"]?([^\s'";]+)['"]?/);
114
+ if (match) {
115
+ return match[1];
116
+ }
117
+ }
118
+ return null;
119
+ }
120
+
121
+ /**
122
+ * Detect RAM information
123
+ */
124
+ function detectRamInfo(): {
125
+ total: number | null;
126
+ available: number | null;
127
+ lowWarning: boolean;
128
+ } {
62
129
  try {
63
- if (process.platform === "linux") {
64
- const meminfo = readFileSync("/proc/meminfo", "utf8");
65
- const match = meminfo.match(/MemTotal:\\s+(\\d+)\\s+kB/);
66
- if (match) {
67
- return Math.round((parseInt(match[1], 10) / 1024 / 1024) * 10) / 10;
68
- }
69
- } else if (process.platform === "darwin") {
70
- const out = require("child_process").execSync("sysctl hw.memsize", { encoding: "utf8" });
71
- const match = out.match(/hw\\.memsize:\\s+(\\d+)/);
72
- if (match) {
73
- return Math.round((parseInt(match[1], 10) / 1024 / 1024 / 1024) * 10) / 10;
74
- }
130
+ const totalBytes = totalmem();
131
+ const freeBytes = freemem();
132
+ const totalGb = Math.round((totalBytes / (1024 ** 3)) * 10) / 10;
133
+ const availableGb = Math.round((freeBytes / (1024 ** 3)) * 10) / 10;
134
+ const LOW_RAM_GB = 16;
135
+
136
+ return {
137
+ total: totalGb,
138
+ available: availableGb,
139
+ lowWarning: totalGb < LOW_RAM_GB || availableGb < 4,
140
+ };
141
+ } catch {
142
+ return { total: null, available: null, lowWarning: false };
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Check if dev/build server is running
148
+ */
149
+ function checkProcessRunning(pattern: string): boolean {
150
+ try {
151
+ if (platform() === "linux") {
152
+ const { execSync } = require("child_process");
153
+ const out = execSync(`pgrep -f "${pattern}" 2>/dev/null || true`, {
154
+ encoding: "utf8",
155
+ timeout: 2000,
156
+ });
157
+ return out.trim().split("\n").filter(Boolean).length > 0;
158
+ } else if (platform() === "darwin") {
159
+ const { execSync } = require("child_process");
160
+ const out = execSync(`pgrep -f "${pattern}" 2>/dev/null || true`, {
161
+ encoding: "utf8",
162
+ timeout: 2000,
163
+ });
164
+ return out.trim().split("\n").filter(Boolean).length > 0;
75
165
  }
76
166
  } catch {
77
- // Fallback: return null
167
+ // Process check failed - assume unknown
78
168
  }
169
+ return false;
170
+ }
171
+
172
+ /**
173
+ * Detect package manager from lock file
174
+ */
175
+ function detectPackageManager(workspacePath: string): string | null {
176
+ if (existsSync(join(workspacePath, "pnpm-lock.yaml"))) return "pnpm";
177
+ if (existsSync(join(workspacePath, "yarn.lock"))) return "yarn";
178
+ if (existsSync(join(workspacePath, "package-lock.json"))) return "npm";
79
179
  return null;
80
180
  }
81
181
 
82
182
  /**
83
- * Detect if workspace is a dev environment
183
+ * Determine workspace mode with proper precedence
184
+ * Never assume production - unknown requires investigation
84
185
  */
85
- export interface WorkspaceEnvResult {
86
- workspacePath: string;
87
- isDevEnv: boolean;
88
- hasDevScript: boolean;
89
- nodeEnv: string | null;
90
- hasEnvFile: boolean;
91
- ramGb: number | null;
92
- detectedAt: number;
186
+ function determineMode(
187
+ processEnv: string | undefined,
188
+ dotenvNodeEnv: string | null,
189
+ evidence: EnvEvidence[]
190
+ ): WorkspaceMode {
191
+ // Explicit process.env takes highest priority (for build processes)
192
+ if (processEnv) {
193
+ const env = processEnv.toLowerCase();
194
+ if (env === "production") {
195
+ evidence.push({
196
+ source: "process.env",
197
+ key: "NODE_ENV",
198
+ value: env,
199
+ timestamp: Date.now(),
200
+ });
201
+ return "production";
202
+ }
203
+ if (env === "development" || env === "dev") {
204
+ evidence.push({
205
+ source: "process.env",
206
+ key: "NODE_ENV",
207
+ value: env,
208
+ timestamp: Date.now(),
209
+ });
210
+ return "development";
211
+ }
212
+ if (env === "test") {
213
+ evidence.push({
214
+ source: "process.env",
215
+ key: "NODE_ENV",
216
+ value: env,
217
+ timestamp: Date.now(),
218
+ });
219
+ return "test";
220
+ }
221
+ }
222
+
223
+ // Check dotenv file if present
224
+ if (dotenvNodeEnv) {
225
+ const env = dotenvNodeEnv.toLowerCase();
226
+ evidence.push({
227
+ source: ".env file",
228
+ key: "NODE_ENV",
229
+ value: env,
230
+ timestamp: Date.now(),
231
+ });
232
+
233
+ if (env === "production") return "production";
234
+ if (env === "development" || env === "dev") return "development";
235
+ if (env === "test") return "test";
236
+ }
237
+
238
+ // Note: presence of dev script is a capability, not proof of mode
239
+ // Mode detection requires explicit NODE_ENV
240
+ return "unknown";
93
241
  }
94
242
 
95
243
  /**
@@ -97,93 +245,208 @@ export interface WorkspaceEnvResult {
97
245
  */
98
246
  export function detectWorkspaceEnv(workspacePath: string): WorkspaceEnvResult {
99
247
  const now = Date.now();
100
- const ramGb = detectRamGb();
101
-
102
- // Check for .env files
103
- const hasEnvFile = existsSync(join(workspacePath, ".env")) ||
104
- existsSync(join(workspacePath, ".env.development")) ||
105
- existsSync(join(workspacePath, ".env.local")) ||
106
- existsSync(join(workspacePath, ".env.dev"));
107
-
108
- // Read NODE_ENV from .env file if present
109
- let nodeEnv: string | null = null;
110
- const envFile = join(workspacePath, ".env");
111
- if (existsSync(envFile)) {
112
- try {
113
- const envContent = readFileSync(envFile, "utf8");
114
- const match = envContent.match(/NODE_ENV=(.+)/);
115
- if (match) {
116
- nodeEnv = match[1].trim().replace(/['"]/g, "");
248
+ const evidence: EnvEvidence[] = [];
249
+
250
+ // Get process.env.NODE_ENV (for build process context)
251
+ const processNodeEnv = process.env.NODE_ENV;
252
+
253
+ // Detect RAM info
254
+ const ramInfo = detectRamInfo();
255
+
256
+ // Check for .env files and parse NODE_ENV
257
+ let dotenvNodeEnv: string | null = null;
258
+ const dotenvFiles = [
259
+ join(workspacePath, ".env"),
260
+ join(workspacePath, ".env.development"),
261
+ join(workspacePath, ".env.dev"),
262
+ ];
263
+ for (const envFile of dotenvFiles) {
264
+ if (existsSync(envFile)) {
265
+ try {
266
+ const content = readFileSync(envFile, "utf8");
267
+ dotenvNodeEnv = parseNodeEnvFromDotenv(content);
268
+ if (dotenvNodeEnv) {
269
+ evidence.push({
270
+ source: ".env file",
271
+ key: "NODE_ENV",
272
+ value: dotenvNodeEnv,
273
+ timestamp: now,
274
+ });
275
+ break;
276
+ }
277
+ } catch {
278
+ // Ignore read errors
117
279
  }
118
- } catch {
119
- // Ignore read errors
120
280
  }
121
281
  }
122
282
 
123
- // Check for dev script in package.json
283
+ // Check for dev/build scripts in package.json
124
284
  let hasDevScript = false;
285
+ let hasBuildScript = false;
125
286
  const pkgJsonPath = join(workspacePath, "package.json");
126
287
  if (existsSync(pkgJsonPath)) {
127
288
  try {
128
289
  const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
129
- hasDevScript = !!(pkg.scripts && (pkg.scripts.dev || pkg.scripts.development));
290
+ hasDevScript = !!(pkg.scripts && (pkg.scripts.dev || pkg.scripts.development || pkg.scripts["dev:next"]));
291
+ hasBuildScript = !!(pkg.scripts && (pkg.scripts.build || pkg.scripts["build:next"]));
292
+ if (hasDevScript) {
293
+ evidence.push({
294
+ source: "package.json",
295
+ key: "scripts.dev",
296
+ value: "present",
297
+ timestamp: now,
298
+ });
299
+ }
130
300
  } catch {
131
301
  // Ignore parse errors
132
302
  }
133
303
  }
134
304
 
135
- // Determine if this is a dev environment
136
- // Dev env if: has .env file OR has dev script OR NODE_ENV=development
137
- const isDevEnv = hasEnvFile || hasDevScript || nodeEnv === "development" || nodeEnv === "dev";
305
+ // Detect package manager
306
+ const packageManager = detectPackageManager(workspacePath);
307
+
308
+ // Check for running dev/build processes
309
+ const devServerRunning = checkProcessRunning("(next dev|nuxt| vite |webpack|remix dev)");
310
+ const buildServerRunning = checkProcessRunning("(next build|nuxt build|webpack build)");
311
+
312
+ // Determine mode - NEVER assume production
313
+ const mode = determineMode(processNodeEnv, dotenvNodeEnv, evidence);
138
314
 
139
- // Store in SQLite
140
- const db = initDatabase();
141
- db.prepare(`
142
- INSERT OR REPLACE INTO workspace_env
143
- (workspace_path, is_dev_env, has_dev_script, node_env, has_env_file, ram_gb, detected_at, updated_at)
144
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
145
- `).run(workspacePath, isDevEnv ? 1 : 0, hasDevScript ? 1 : 0, nodeEnv, hasEnvFile ? 1 : 0, ramGb, now, now);
315
+ // Determine build permission
316
+ let buildAllowed = false;
317
+ if (mode === "production") {
318
+ buildAllowed = true; // Production build is allowed
319
+ } else if (mode === "development") {
320
+ buildAllowed = !ramInfo.lowWarning; // Allow if not low RAM
321
+ } else {
322
+ buildAllowed = false; // Unknown requires investigation
323
+ }
146
324
 
147
- db.close();
325
+ // Determine low RAM warning
326
+ const lowRamWarning = ramInfo.lowWarning;
148
327
 
149
- return {
328
+ // Build result
329
+ const result: WorkspaceEnvResult = {
150
330
  workspacePath,
151
- isDevEnv,
331
+ mode,
332
+ evidence,
152
333
  hasDevScript,
153
- nodeEnv,
154
- hasEnvFile,
155
- ramGb,
334
+ hasBuildScript,
335
+ packageManager,
336
+ devServerRunning,
337
+ buildServerRunning,
338
+ ramTotalGb: ramInfo.total,
339
+ ramAvailableGb: ramInfo.available,
340
+ ramEffectiveLimitGb: ramInfo.total, // Could be enhanced with cgroup detection
341
+ lowRamWarning,
342
+ buildAllowed,
156
343
  detectedAt: now,
157
344
  };
345
+
346
+ // Store in SQLite (synchronously, exception-safe)
347
+ let db: DatabaseSync | null = null;
348
+ try {
349
+ db = initDatabase();
350
+ db.prepare(`
351
+ INSERT OR REPLACE INTO workspace_env
352
+ (workspace_path, mode, evidence, has_dev_script, has_build_script, package_manager,
353
+ dev_server_running, build_server_running, ram_total_gb, ram_available_gb,
354
+ ram_effective_limit_gb, low_ram_warning, build_allowed, detected_at, updated_at)
355
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
356
+ `).run(
357
+ workspacePath,
358
+ mode,
359
+ JSON.stringify(evidence),
360
+ hasDevScript ? 1 : 0,
361
+ hasBuildScript ? 1 : 0,
362
+ packageManager,
363
+ devServerRunning ? 1 : 0,
364
+ buildServerRunning ? 1 : 0,
365
+ ramInfo.total,
366
+ ramInfo.available,
367
+ ramInfo.total,
368
+ lowRamWarning ? 1 : 0,
369
+ buildAllowed ? 1 : 0,
370
+ now,
371
+ now
372
+ );
373
+ } catch (err) {
374
+ // SQLite failure - continue with in-memory result
375
+ console.error("[WorkspaceEnv] SQLite persistence failed:", err);
376
+ } finally {
377
+ if (db) {
378
+ try {
379
+ db.close();
380
+ } catch {
381
+ // Ignore close errors
382
+ }
383
+ }
384
+ }
385
+
386
+ return result;
158
387
  }
159
388
 
160
389
  /**
161
390
  * Load cached workspace environment
162
391
  */
163
392
  export function loadWorkspaceEnv(workspacePath: string): WorkspaceEnvResult | null {
164
- const db = initDatabase();
165
- const row = db.prepare("SELECT * FROM workspace_env WHERE workspace_path = ?").get(workspacePath) as {
166
- workspace_path: string;
167
- is_dev_env: number;
168
- has_dev_script: number;
169
- node_env: string | null;
170
- has_env_file: number;
171
- ram_gb: number | null;
172
- detected_at: number;
173
- } | undefined;
174
- db.close();
175
-
176
- if (!row) return null;
177
-
178
- return {
179
- workspacePath: row.workspace_path,
180
- isDevEnv: row.is_dev_env === 1,
181
- hasDevScript: row.has_dev_script === 1,
182
- nodeEnv: row.node_env,
183
- hasEnvFile: row.has_env_file === 1,
184
- ramGb: row.ram_gb,
185
- detectedAt: row.detected_at,
186
- };
393
+ let db: DatabaseSync | null = null;
394
+ try {
395
+ db = initDatabase();
396
+ const row = db.prepare("SELECT * FROM workspace_env WHERE workspace_path = ?").get(workspacePath) as {
397
+ workspace_path: string;
398
+ mode: string;
399
+ evidence: string;
400
+ has_dev_script: number;
401
+ has_build_script: number;
402
+ package_manager: string | null;
403
+ dev_server_running: number;
404
+ build_server_running: number;
405
+ ram_total_gb: number | null;
406
+ ram_available_gb: number | null;
407
+ ram_effective_limit_gb: number | null;
408
+ low_ram_warning: number;
409
+ build_allowed: number;
410
+ detected_at: number;
411
+ } | undefined;
412
+
413
+ if (!row) return null;
414
+
415
+ // Check cache freshness (5 minutes for dev workspaces)
416
+ const ageMs = Date.now() - row.detected_at;
417
+ const MAX_CACHE_AGE_MS = 5 * 60 * 1000; // 5 minutes
418
+
419
+ if (ageMs > MAX_CACHE_AGE_MS) {
420
+ return null; // Cache stale
421
+ }
422
+
423
+ return {
424
+ workspacePath: row.workspace_path,
425
+ mode: row.mode as WorkspaceMode,
426
+ evidence: JSON.parse(row.evidence || "[]"),
427
+ hasDevScript: row.has_dev_script === 1,
428
+ hasBuildScript: row.has_build_script === 1,
429
+ packageManager: row.package_manager,
430
+ devServerRunning: row.dev_server_running === 1,
431
+ buildServerRunning: row.build_server_running === 1,
432
+ ramTotalGb: row.ram_total_gb,
433
+ ramAvailableGb: row.ram_available_gb,
434
+ ramEffectiveLimitGb: row.ram_effective_limit_gb,
435
+ lowRamWarning: row.low_ram_warning === 1,
436
+ buildAllowed: row.build_allowed === 1,
437
+ detectedAt: row.detected_at,
438
+ };
439
+ } catch {
440
+ return null;
441
+ } finally {
442
+ if (db) {
443
+ try {
444
+ db.close();
445
+ } catch {
446
+ // Ignore close errors
447
+ }
448
+ }
449
+ }
187
450
  }
188
451
 
189
452
  /**
@@ -193,86 +456,77 @@ export function getWorkspaceEnv(workspacePath: string): WorkspaceEnvResult {
193
456
  // Try to load cached result first
194
457
  const cached = loadWorkspaceEnv(workspacePath);
195
458
  if (cached) {
196
- // Check if cache is recent (within 1 hour)
197
- const ageMs = Date.now() - cached.detectedAt;
198
- if (ageMs < 60 * 60 * 1000) {
199
- return cached;
200
- }
459
+ return cached;
201
460
  }
202
461
  // Detect fresh
203
462
  return detectWorkspaceEnv(workspacePath);
204
463
  }
205
464
 
206
465
  /**
207
- * Check if agent should prefer dev mode (no build)
466
+ * Revalidate workspace environment before impactful commands
208
467
  */
209
- export function shouldUseDevMode(workspacePath: string): {
210
- preferDev: boolean;
211
- reason: string;
212
- ramGb: number | null;
213
- } {
468
+ export function revalidateWorkspaceEnv(workspacePath: string): WorkspaceEnvResult {
469
+ // Force fresh detection for command decisions
470
+ return detectWorkspaceEnv(workspacePath);
471
+ }
472
+
473
+ /**
474
+ * Format workspace env as agent-readable message
475
+ */
476
+ export function formatEnvForAgent(workspacePath: string): string {
214
477
  const env = getWorkspaceEnv(workspacePath);
215
478
 
216
- // Low RAM threshold
217
- const LOW_RAM_GB = 16;
479
+ let message = `[Workspace Environment]\n`;
480
+ message += `Mode: ${env.mode.toUpperCase()}\n`;
218
481
 
219
- if (!env.isDevEnv) {
220
- return {
221
- preferDev: false,
222
- reason: "Not detected as dev environment",
223
- ramGb: env.ramGb,
224
- };
482
+ if (env.evidence.length > 0) {
483
+ const sources = env.evidence.map((e) => `${e.source}:${e.value}`).join(", ");
484
+ message += `Evidence: ${sources}\n`;
225
485
  }
226
486
 
227
- if (env.ramGb !== null && env.ramGb < LOW_RAM_GB) {
228
- return {
229
- preferDev: true,
230
- reason: `Low RAM (${env.ramGb}GB < ${LOW_RAM_GB}GB) - avoid parallel build + dev`,
231
- ramGb: env.ramGb,
232
- };
487
+ message += `Dev script: ${env.hasDevScript ? "yes" : "no"}\n`;
488
+ message += `Build script: ${env.hasBuildScript ? "yes" : "no"}\n`;
489
+
490
+ if (env.devServerRunning) {
491
+ message += `Dev server: running\n`;
492
+ }
493
+ if (env.buildServerRunning) {
494
+ message += `Build server: running\n`;
233
495
  }
234
496
 
235
- if (env.hasDevScript) {
236
- return {
237
- preferDev: true,
238
- reason: `Dev script detected - hot reload available`,
239
- ramGb: env.ramGb,
240
- };
497
+ if (env.ramTotalGb !== null) {
498
+ const ramStr = env.ramAvailableGb !== null
499
+ ? `${env.ramAvailableGb}/${env.ramTotalGb}GB available`
500
+ : `${env.ramTotalGb}GB total`;
501
+ message += `RAM: ${ramStr}\n`;
502
+ if (env.lowRamWarning) {
503
+ message += `WARNING: Low RAM - avoid parallel build + dev\n`;
504
+ }
241
505
  }
242
506
 
243
- return {
244
- preferDev: false,
245
- reason: "Dev environment but no dev script found",
246
- ramGb: env.ramGb,
247
- };
507
+ message += `\n`;
508
+ message += `Build allowed: ${env.buildAllowed ? "yes" : "no - requires explicit request"}\n`;
509
+
510
+ if (env.mode === "unknown") {
511
+ message += `\nNOTE: Environment unknown. Explicitly ask user before build.\n`;
512
+ }
513
+
514
+ return message;
248
515
  }
249
516
 
250
517
  /**
251
- * Format workspace env as agent-readable message
518
+ * Format workspace env as compact status line for UI
252
519
  */
253
- export function formatEnvForAgent(workspacePath: string): string {
254
- const env = shouldUseDevMode(workspacePath);
255
- const details = getWorkspaceEnv(workspacePath);
520
+ export function formatEnvStatusLine(env: WorkspaceEnvResult): string {
521
+ const mode = env.mode.toUpperCase();
522
+ const devStatus = env.devServerRunning ? "running" : (env.hasDevScript ? "available" : "none");
523
+ const buildStatus = env.buildServerRunning ? "running" : (env.hasBuildScript ? "explicit request" : "none");
524
+ const ramStr = env.ramAvailableGb !== null ? `${env.ramAvailableGb}/${env.ramTotalGb}` : "";
256
525
 
257
- let message = `[Workspace Environment]\n`;
258
- message += `- Path: ${workspacePath}\n`;
259
- message += `- Detected: ${details.isDevEnv ? "Development" : "Production/Unknown"}\n`;
260
- message += `- Has dev script: ${details.hasDevScript ? "Yes" : "No"}\n`;
261
- message += `- NODE_ENV: ${details.nodeEnv || "not set"}\n`;
262
- message += `- Has .env file: ${details.hasEnvFile ? "Yes" : "No"}\n`;
263
- message += `- RAM: ${details.ramGb ? `${details.ramGb}GB` : "unknown"}\n`;
264
- message += `\n`;
265
-
266
- if (env.preferDev) {
267
- message += `RECOMMENDATION: Use \`dev\` mode (no build needed)\n`;
268
- message += `Reason: ${env.reason}\n`;
269
- message += `→ Run: \`bun run dev\` (or \`npm run dev\`)\n`;
270
- message += `→ DON'T: Run \`build\` unless explicitly requested\n`;
271
- } else {
272
- message += `RECOMMENDATION: Build may be needed\n`;
273
- message += `Reason: ${env.reason}\n`;
274
- message += `→ Use: \`bun run build\` before running\n`;
526
+ let status = `Workspace: ${mode} | dev server: ${devStatus} | build: ${buildStatus}`;
527
+ if (ramStr) {
528
+ status += ` | RAM: ${ramStr}`;
275
529
  }
276
530
 
277
- return message;
531
+ return status;
278
532
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/cookie-sanitizer",
3
- "version": "1.1.367",
3
+ "version": "1.1.369",
4
4
  "description": "pi-harness-runtime — cookie sanitizer (provider-quota cookie file normaliser)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",