@monoes/monodesign 1.2.0 → 1.2.2

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.
@@ -149,8 +149,8 @@ function list(cwd) {
149
149
  const local = readRawDetectionConfig(cwd, { local: true });
150
150
  return [
151
151
  'Monodesign detector ignores',
152
- ` shared file: ${path.relative(cwd, getConfigPath(cwd)) || getConfigPath(cwd)}`,
153
- ` local file: ${path.relative(cwd, getLocalConfigPath(cwd)) || getLocalConfigPath(cwd)}`,
152
+ ` shared file: ${path.relative(cwd, getConfigPath(cwd)).split(path.sep).join('/') || getConfigPath(cwd)}`,
153
+ ` local file: ${path.relative(cwd, getLocalConfigPath(cwd)).split(path.sep).join('/') || getLocalConfigPath(cwd)}`,
154
154
  '',
155
155
  formatConfig('Merged', merged),
156
156
  '',
@@ -203,7 +203,7 @@ function addRule(cwd, args) {
203
203
  const config = readScopeConfig(cwd, local);
204
204
  if (!config.ignoreRules.includes(rule)) config.ignoreRules.push(rule);
205
205
  const target = writeScopeConfig(cwd, config, local);
206
- return `Added ${rule} to ${local ? 'local' : 'shared'} detector ignoreRules (${path.relative(cwd, target) || target}).`;
206
+ return `Added ${rule} to ${local ? 'local' : 'shared'} detector ignoreRules (${path.relative(cwd, target).split(path.sep).join('/') || target}).`;
207
207
  }
208
208
 
209
209
  function addFile(cwd, args) {
@@ -213,7 +213,7 @@ function addFile(cwd, args) {
213
213
  const config = readScopeConfig(cwd, local);
214
214
  if (!config.ignoreFiles.includes(glob)) config.ignoreFiles.push(glob);
215
215
  const target = writeScopeConfig(cwd, config, local);
216
- return `Added ${glob} to ${local ? 'local' : 'shared'} detector ignoreFiles (${path.relative(cwd, target) || target}).`;
216
+ return `Added ${glob} to ${local ? 'local' : 'shared'} detector ignoreFiles (${path.relative(cwd, target).split(path.sep).join('/') || target}).`;
217
217
  }
218
218
 
219
219
  function addValue(cwd, args) {
@@ -236,7 +236,7 @@ function addValue(cwd, args) {
236
236
  config.ignoreValues.push(entry);
237
237
  }
238
238
  const target = writeScopeConfig(cwd, config, local);
239
- return `Added ${parsed.rule}=${parsed.value} to ${local ? 'local' : 'shared'} detector ignoreValues (${path.relative(cwd, target) || target}).`;
239
+ return `Added ${parsed.rule}=${parsed.value} to ${local ? 'local' : 'shared'} detector ignoreValues (${path.relative(cwd, target).split(path.sep).join('/') || target}).`;
240
240
  }
241
241
 
242
242
  function removeFromScopes(cwd, args, remover) {
@@ -248,7 +248,7 @@ function removeFromScopes(cwd, args, remover) {
248
248
  const count = remover(config, rest);
249
249
  if (count > 0) {
250
250
  const target = writeScopeConfig(cwd, config, isLocal);
251
- removed.push(`${count} from ${isLocal ? 'local' : 'shared'} (${path.relative(cwd, target) || target})`);
251
+ removed.push(`${count} from ${isLocal ? 'local' : 'shared'} (${path.relative(cwd, target).split(path.sep).join('/') || target})`);
252
252
  }
253
253
  }
254
254
  return removed.length ? `Removed ${removed.join(', ')}.` : 'No matching detector ignore found.';
@@ -3568,6 +3568,13 @@ function checkElementImageDimensions(el, style) {
3568
3568
  }
3569
3569
 
3570
3570
  function checkElementImageDimensionsDOM(el) {
3571
+ // Only <img> can have this problem. The static engine gets this for free —
3572
+ // it registers the rule with `selector: 'img'` (engines/static-html/
3573
+ // detect-html.mjs) — but the DOM walker calls every check against every
3574
+ // element, so the tag filter has to live here. Without it this rule fired on
3575
+ // <head>, <meta>, <title> and <style>: a clean 84-line fixture reported 43
3576
+ // findings, all of them "an <img> ships without width and height".
3577
+ if (el.tagName !== 'IMG') return [];
3571
3578
  const hasWidthAttr = el.getAttribute('width') != null;
3572
3579
  const hasHeightAttr = el.getAttribute('height') != null;
3573
3580
  // Browser computed `height` always resolves to a used px value after layout,
@@ -257,7 +257,7 @@ async function runFix(targets, options = {}) {
257
257
  const diffs = [];
258
258
  if (dryRun) {
259
259
  for (const [p, b] of changedFiles) {
260
- diffs.push({ file: p, diff: unifiedDiff(b.original, b.content, path.relative(cwd, p) || p) });
260
+ diffs.push({ file: p, diff: unifiedDiff(b.original, b.content, path.relative(cwd, p).split(path.sep).join('/') || p) });
261
261
  }
262
262
  } else {
263
263
  for (const [p, b] of changedFiles) writeFileAtomic(p, b.content);
@@ -2845,6 +2845,13 @@ function checkElementImageDimensions(el, style) {
2845
2845
  }
2846
2846
 
2847
2847
  function checkElementImageDimensionsDOM(el) {
2848
+ // Only <img> can have this problem. The static engine gets this for free —
2849
+ // it registers the rule with `selector: 'img'` (engines/static-html/
2850
+ // detect-html.mjs) — but the DOM walker calls every check against every
2851
+ // element, so the tag filter has to live here. Without it this rule fired on
2852
+ // <head>, <meta>, <title> and <style>: a clean 84-line fixture reported 43
2853
+ // findings, all of them "an <img> ships without width and height".
2854
+ if (el.tagName !== 'IMG') return [];
2848
2855
  const hasWidthAttr = el.getAttribute('width') != null;
2849
2856
  const hasHeightAttr = el.getAttribute('height') != null;
2850
2857
  // Browser computed `height` always resolves to a used px value after layout,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monodesign",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Frontend design intelligence for AI agents \u2014 design tokens, antipattern detection, and the monodesign skill",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "scripts": {
23
23
  "build": "tsc",
24
- "test": "node --experimental-strip-types --test 'tests/*.test.*' 'tests/lib/*.test.*'",
24
+ "test": "node scripts/run-tests.mjs",
25
25
  "sync": "node scripts/sync-skill.mjs",
26
26
  "prepare": "node scripts/sync-skill.mjs"
27
27
  },
@@ -45,7 +45,7 @@
45
45
  "marked": "^18.0.0"
46
46
  },
47
47
  "optionalDependencies": {
48
- "@monoes/monobrowse": "^1.0.3",
48
+ "@monoes/monobrowse": "^1.0.6",
49
49
  "puppeteer": "^25.1.0"
50
50
  }
51
51
  }
@@ -93,7 +93,7 @@ function latestCritique(cwd) {
93
93
  p0: numKey('p0', 'p0_count'),
94
94
  p1: numKey('p1', 'p1_count'),
95
95
  timestamp: get('timestamp'),
96
- file: path.relative(cwd, path.join(dir, newest)),
96
+ file: path.relative(cwd, path.join(dir, newest)).split(path.sep).join('/'),
97
97
  trend,
98
98
  openP0,
99
99
  };
@@ -75,10 +75,10 @@ export function loadContext(cwd = process.cwd(), options = {}) {
75
75
  return {
76
76
  hasProduct: !!product,
77
77
  product,
78
- productPath: productPath ? path.relative(absCwd, productPath) : null,
78
+ productPath: productPath ? path.relative(absCwd, productPath).split(path.sep).join('/') : null,
79
79
  hasDesign: !!design,
80
80
  design,
81
- designPath: designPath ? path.relative(absCwd, designPath) : null,
81
+ designPath: designPath ? path.relative(absCwd, designPath).split(path.sep).join('/') : null,
82
82
  contextDir: resolved.contextDir,
83
83
  productContextDir: productPath ? path.dirname(productPath) : null,
84
84
  designContextDir: designPath ? path.dirname(designPath) : null,
@@ -186,7 +186,7 @@ function resolveProject(cwd = process.cwd(), options = {}) {
186
186
  }
187
187
 
188
188
  function isPathInside(candidate, root) {
189
- const rel = path.relative(root, candidate);
189
+ const rel = path.relative(root, candidate).split(path.sep).join('/');
190
190
  return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
191
191
  }
192
192
 
@@ -339,9 +339,9 @@ function contextSourceStatus(filePath, repoRoot, projectRoot) {
339
339
 
340
340
  function contextSourcePath(filePath, repoRoot) {
341
341
  if (!filePath) return null;
342
- const rel = path.relative(repoRoot, filePath);
342
+ const rel = path.relative(repoRoot, filePath).split(path.sep).join('/');
343
343
  if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) {
344
- return rel.split(path.sep).join('/');
344
+ return rel;
345
345
  }
346
346
  return filePath;
347
347
  }
@@ -447,9 +447,13 @@ function findTargetExample(repoRoot, projectRoot) {
447
447
  }
448
448
 
449
449
  function resolveWorkspaceProjectRoot(repoRoot, targetDir) {
450
- const rel = path.relative(repoRoot, targetDir);
450
+ const rel = path.relative(repoRoot, targetDir).split(path.sep).join('/');
451
451
  if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return repoRoot;
452
- const relSegments = rel.split(path.sep).filter(Boolean);
452
+ // rel is POSIX-normalised above, so split on '/' — splitting on path.sep here
453
+ // returned the whole string as a single segment on Windows, no workspace
454
+ // pattern ever matched, and every child project silently resolved to the
455
+ // repo root instead.
456
+ const relSegments = rel.split('/').filter(Boolean);
453
457
  const patterns = readWorkspacePatterns(repoRoot);
454
458
  const excluded = isExcludedByWorkspacePattern(relSegments, patterns);
455
459
  if (!excluded) {
@@ -64,7 +64,7 @@ export function slugFromTarget(resolved, { cwd = process.cwd() } = {}) {
64
64
  // File path. Make it project-relative so two devs critiquing the same
65
65
  // checkout get the same slug regardless of where their repo is cloned.
66
66
  const abs = path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
67
- let rel = path.relative(cwd, abs);
67
+ let rel = path.relative(cwd, abs).split(path.sep).join('/');
68
68
  // If the target is outside cwd, fall back to the basename so we still
69
69
  // produce a stable slug (vs the absolute path, which would include
70
70
  // home dirs / usernames).
@@ -231,7 +231,7 @@ export function mirrorToMemory({ slug, meta = {}, filePath, cwd = process.cwd(),
231
231
  p1: asFiniteNumber(meta.p1_count ?? meta.p1),
232
232
  date: now.toISOString(),
233
233
  slug,
234
- path: filePath ? path.relative(cwd, filePath) : null,
234
+ path: filePath ? path.relative(cwd, filePath).split(path.sep).join('/') : null,
235
235
  };
236
236
  const project = kebab(path.basename(path.resolve(cwd))) || 'project';
237
237
  const key = `${project}-${slug}`;
@@ -340,7 +340,7 @@ export function formatRecall(recall, { cwd = process.cwd() } = {}) {
340
340
  if (!issues.p0.length && !issues.p1.length) {
341
341
  lines.push('- No open P0/P1 lines found in the latest snapshot.');
342
342
  }
343
- lines.push(`- Snapshot: ${path.relative(cwd, latest.path)}`);
343
+ lines.push(`- Snapshot: ${path.relative(cwd, latest.path).split(path.sep).join('/')}`);
344
344
  return lines.join('\n');
345
345
  }
346
346
 
@@ -186,7 +186,7 @@ function walk(root, dir, depth, visit) {
186
186
  body = buf.slice(0, n).toString('utf-8');
187
187
  } finally { fs.closeSync(fd); }
188
188
  } catch { continue; }
189
- visit(abs, path.relative(root, abs), body);
189
+ visit(abs, path.relative(root, abs).split(path.sep).join('/'), body);
190
190
  }
191
191
  }
192
192
 
@@ -275,9 +275,9 @@ function statusReport(cwd) {
275
275
  const cfg = readConfig(cwd);
276
276
  const envKill = process.env.MONODESIGN_HOOK_DISABLED;
277
277
  const envState = envKill ? `MONODESIGN_HOOK_DISABLED=${envKill}` : 'unset';
278
- const cfgPath = path.relative(cwd, getConfigPath(cwd)) || '.monodesign/config.json';
279
- const localPath = path.relative(cwd, getLocalConfigPath(cwd)) || '.monodesign/config.local.json';
280
- const cachePath = path.relative(cwd, getCachePath(cwd)) || '.monodesign/hook.cache.json';
278
+ const cfgPath = path.relative(cwd, getConfigPath(cwd)).split(path.sep).join('/') || '.monodesign/config.json';
279
+ const localPath = path.relative(cwd, getLocalConfigPath(cwd)).split(path.sep).join('/') || '.monodesign/config.local.json';
280
+ const cachePath = path.relative(cwd, getCachePath(cwd)).split(path.sep).join('/') || '.monodesign/hook.cache.json';
281
281
  const fileState = (info, relPath, absent) => {
282
282
  if (info.malformed) return `${relPath} (malformed; ignored)`;
283
283
  if (info.exists) return relPath;
@@ -306,14 +306,14 @@ function setEnabled(cwd, value) {
306
306
  config.enabled = value;
307
307
  const target = writeHookConfig(cwd, config);
308
308
  if (!value) {
309
- return `Design hook disabled for this project (wrote ${path.relative(cwd, target) || target}).`;
309
+ return `Design hook disabled for this project (wrote ${path.relative(cwd, target).split(path.sep).join('/') || target}).`;
310
310
  }
311
311
 
312
312
  const localTarget = writeHookConfig(cwd, { consent: 'accepted' }, { local: true });
313
313
  const repaired = repairHookManifests(cwd);
314
314
  const parts = [
315
- `Design hook enabled for this project (wrote ${path.relative(cwd, target) || target}).`,
316
- `Recorded local hook consent in ${path.relative(cwd, localTarget) || localTarget}.`,
315
+ `Design hook enabled for this project (wrote ${path.relative(cwd, target).split(path.sep).join('/') || target}).`,
316
+ `Recorded local hook consent in ${path.relative(cwd, localTarget).split(path.sep).join('/') || localTarget}.`,
317
317
  ];
318
318
  if (repaired.written.length > 0) {
319
319
  parts.push(`Installed or repaired hook manifests for: ${repaired.written.join(', ')}.`);
@@ -323,7 +323,7 @@ function setEnabled(cwd, value) {
323
323
  parts.push('No installed provider skill folders found to repair.');
324
324
  }
325
325
  if (repaired.backups.length > 0) {
326
- parts.push(`Backed up malformed manifest(s): ${repaired.backups.map((filePath) => path.relative(cwd, filePath) || filePath).join(', ')}.`);
326
+ parts.push(`Backed up malformed manifest(s): ${repaired.backups.map((filePath) => path.relative(cwd, filePath).split(path.sep).join('/') || filePath).join(', ')}.`);
327
327
  }
328
328
  return parts.join(' ');
329
329
  }
@@ -596,7 +596,7 @@ function addIgnoreValue(cwd, args) {
596
596
 
597
597
  const target = writeDetectorConfig(cwd, config, { local });
598
598
  const scope = local ? 'local detector.ignoreValues' : 'shared detector.ignoreValues';
599
- return `Added ${parsed.rule}=${parsed.value} to ${scope} (${path.relative(cwd, target) || target}).`;
599
+ return `Added ${parsed.rule}=${parsed.value} to ${scope} (${path.relative(cwd, target).split(path.sep).join('/') || target}).`;
600
600
  }
601
601
 
602
602
  function reset(cwd) {
@@ -613,7 +613,7 @@ function reset(cwd) {
613
613
  } else {
614
614
  fs.writeFileSync(filePath, JSON.stringify(rest, null, 2) + '\n');
615
615
  }
616
- removed.push(path.relative(cwd, filePath) || filePath);
616
+ removed.push(path.relative(cwd, filePath).split(path.sep).join('/') || filePath);
617
617
  } catch { /* ignore */ }
618
618
  }
619
619
  // State files are wholly ours; delete outright.
@@ -621,7 +621,7 @@ function reset(cwd) {
621
621
  try {
622
622
  if (fs.existsSync(filePath)) {
623
623
  fs.unlinkSync(filePath);
624
- removed.push(path.relative(cwd, filePath) || filePath);
624
+ removed.push(path.relative(cwd, filePath).split(path.sep).join('/') || filePath);
625
625
  }
626
626
  } catch { /* ignore */ }
627
627
  }
@@ -320,7 +320,7 @@ function escapeRegExp(value) {
320
320
 
321
321
  function relativePath(filePath, cwd) {
322
322
  try {
323
- const rel = path.relative(cwd, filePath);
323
+ const rel = path.relative(cwd, filePath).split(path.sep).join('/');
324
324
  if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return filePath;
325
325
  return rel.split(path.sep).join('/');
326
326
  } catch {
@@ -330,7 +330,7 @@ function relativePath(filePath, cwd) {
330
330
 
331
331
  function isInsideProject(filePath, cwd) {
332
332
  try {
333
- const rel = path.relative(cwd, filePath);
333
+ const rel = path.relative(cwd, filePath).split(path.sep).join('/');
334
334
  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
335
335
  } catch {
336
336
  return false;
@@ -1015,7 +1015,7 @@ function quoteCommandArg(value) {
1015
1015
 
1016
1016
  function relativize(filePath, cwd) {
1017
1017
  try {
1018
- const rel = path.relative(cwd, filePath);
1018
+ const rel = path.relative(cwd, filePath).split(path.sep).join('/');
1019
1019
  if (!rel || rel.startsWith('..')) return filePath;
1020
1020
  return rel.split(path.sep).join('/');
1021
1021
  } catch {
@@ -1226,7 +1226,7 @@ function hasPathTraversal(filePath) {
1226
1226
  function isInsideProject(filePath, projectCwd) {
1227
1227
  if (!filePath || !projectCwd || hasPathTraversal(filePath)) return false;
1228
1228
  try {
1229
- const rel = path.relative(projectCwd, filePath);
1229
+ const rel = path.relative(projectCwd, filePath).split(path.sep).join('/');
1230
1230
  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
1231
1231
  } catch {
1232
1232
  return false;
@@ -338,7 +338,7 @@ export function normalizeManualApplyEvidencePath(evidencePath, cwd = process.cwd
338
338
  if (!evidencePath || typeof evidencePath !== 'string') return null;
339
339
  const fullPath = path.isAbsolute(evidencePath) ? evidencePath : path.resolve(cwd, evidencePath);
340
340
  const evidenceDir = manualApplyEvidenceDir(cwd);
341
- const relative = path.relative(evidenceDir, fullPath);
341
+ const relative = path.relative(evidenceDir, fullPath).split(path.sep).join('/');
342
342
  if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null;
343
343
  if (path.extname(relative) !== '.json') return null;
344
344
  return fullPath;
@@ -837,7 +837,7 @@ export function collectManualApplyFiles(batch, extraFiles = [], cwd = process.cw
837
837
  function normalizeProjectFile(file, cwd = process.cwd()) {
838
838
  if (!file || typeof file !== 'string') return null;
839
839
  const absolute = path.isAbsolute(file) ? file : path.resolve(cwd, file);
840
- const relative = path.relative(cwd, absolute);
840
+ const relative = path.relative(cwd, absolute).split(path.sep).join('/');
841
841
  if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null;
842
842
  return relative;
843
843
  }
@@ -927,7 +927,7 @@ export function summarizeManualDiagnostics(items, cwd = process.cwd()) {
927
927
  export function summarizeManualLogFile(file, cwd = process.cwd()) {
928
928
  if (!file || typeof file !== 'string') return undefined;
929
929
  if (!path.isAbsolute(file)) return file;
930
- const relative = path.relative(cwd, file);
930
+ const relative = path.relative(cwd, file).split(path.sep).join('/');
931
931
  return relative && !relative.startsWith('..') && !path.isAbsolute(relative) ? relative : file;
932
932
  }
933
933
 
@@ -265,7 +265,7 @@ export function resolveSourceFile(sourceFile, cwd = process.cwd()) {
265
265
  throw new Error('Invalid svelte-component source file');
266
266
  }
267
267
  const full = path.resolve(cwd, sourceFile);
268
- const rel = path.relative(cwd, full);
268
+ const rel = path.relative(cwd, full).split(path.sep).join('/');
269
269
  if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) {
270
270
  throw new Error('Svelte-component source file escapes project root');
271
271
  }
@@ -119,7 +119,7 @@ Output (JSON):
119
119
  }
120
120
 
121
121
  const { file: targetFile, content, lines } = found;
122
- const relFile = path.relative(process.cwd(), targetFile);
122
+ const relFile = path.relative(process.cwd(), targetFile).split(path.sep).join('/');
123
123
  const previewBlock = findMarkerBlock(id, lines);
124
124
  const sourceShadowPreview = previewBlock
125
125
  ? readSourceShadowPreviewMeta(content, id)
@@ -224,7 +224,7 @@ function buildRepairBatch(batch, repair) {
224
224
  function normalizeProjectSourcePath(cwd, file, opts = {}) {
225
225
  if (!file || typeof file !== 'string') return null;
226
226
  const absolute = path.isAbsolute(file) ? file : path.resolve(cwd, file);
227
- const relative = path.relative(cwd, absolute);
227
+ const relative = path.relative(cwd, absolute).split(path.sep).join('/');
228
228
  if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null;
229
229
  if (opts.requireExists && !fs.existsSync(absolute)) return null;
230
230
  if (isGeneratedFile(absolute, { cwd })) return null;
@@ -622,7 +622,7 @@ function scanRollbackDir(dir, cwd, out, seenDirs, seenFiles, depth) {
622
622
  try { realFile = fs.realpathSync(absolute); } catch { continue; }
623
623
  if (seenFiles.has(realFile)) continue;
624
624
  seenFiles.add(realFile);
625
- const relative = path.relative(cwd, absolute);
625
+ const relative = path.relative(cwd, absolute).split(path.sep).join('/');
626
626
  if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) continue;
627
627
  out.push(relative);
628
628
  }
@@ -534,7 +534,7 @@ function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, m
534
534
  }
535
535
 
536
536
  function isPathInsideOrEqual(cwd, file) {
537
- const relative = path.relative(path.resolve(cwd), path.resolve(file));
537
+ const relative = path.relative(path.resolve(cwd), path.resolve(file)).split(path.sep).join('/');
538
538
  return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
539
539
  }
540
540
 
@@ -166,7 +166,7 @@ Output (JSON):
166
166
  console.error(JSON.stringify({
167
167
  error: 'file_is_generated',
168
168
  fallback: 'agent-driven',
169
- file: path.relative(process.cwd(), path.resolve(process.cwd(), targetFile)),
169
+ file: path.relative(process.cwd(), path.resolve(process.cwd(), targetFile)).split(path.sep).join('/'),
170
170
  }));
171
171
  process.exit(1);
172
172
  }
@@ -179,7 +179,7 @@ Output (JSON):
179
179
  console.error(JSON.stringify({
180
180
  error: 'element_ambiguous',
181
181
  fallback: 'agent-driven',
182
- file: path.relative(process.cwd(), targetFile),
182
+ file: path.relative(process.cwd(), targetFile).split(path.sep).join('/'),
183
183
  candidates: resolved.candidates.map((c) => ({
184
184
  startLine: c.startLine + 1,
185
185
  endLine: c.endLine + 1,
@@ -64,7 +64,7 @@ export function buildManualEditEvidence({ cwd = process.cwd(), pageUrl = null }
64
64
  ops,
65
65
  context: {
66
66
  cwd,
67
- bufferPath: path.relative(cwd, getBufferPath(cwd)),
67
+ bufferPath: path.relative(cwd, getBufferPath(cwd)).split(path.sep).join('/'),
68
68
  totalEntries: entries.length,
69
69
  totalOps: opCount,
70
70
  },
@@ -157,7 +157,7 @@ function analyzeSourceHint(op, cwd) {
157
157
  const hint = normalizeSourceHint(op.sourceHint);
158
158
  if (!hint.file) return null;
159
159
  const file = path.resolve(cwd, hint.file);
160
- const relativeFile = path.relative(cwd, file);
160
+ const relativeFile = path.relative(cwd, file).split(path.sep).join('/');
161
161
  if (!isPathInsideOrEqual(cwd, file)) {
162
162
  return { ...hint, status: 'outside_cwd', relativeFile: hint.file };
163
163
  }
@@ -254,7 +254,7 @@ function maybeAddSearchFile(file, cwd, seenFiles, out) {
254
254
  if (isGeneratedFile(file, { cwd })) return;
255
255
  let content;
256
256
  try { content = fs.readFileSync(file, 'utf-8'); } catch { return; }
257
- out.push({ file, relativeFile: path.relative(cwd, file), content, lines: content.split('\n') });
257
+ out.push({ file, relativeFile: path.relative(cwd, file).split(path.sep).join('/'), content, lines: content.split('\n') });
258
258
  }
259
259
 
260
260
  function findLiteralMatches(searchFiles, needle, { max }) {
@@ -340,7 +340,7 @@ function matchForIndex(file, index, kind, needle) {
340
340
  }
341
341
 
342
342
  function isPathInsideOrEqual(cwd, file) {
343
- const rel = path.relative(path.resolve(cwd), path.resolve(file));
343
+ const rel = path.relative(path.resolve(cwd), path.resolve(file)).split(path.sep).join('/');
344
344
  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
345
345
  }
346
346
 
@@ -789,7 +789,7 @@ function sessionFileMetadataFromPollReply(file) {
789
789
  let full;
790
790
  try {
791
791
  full = path.resolve(process.cwd(), normalized);
792
- const rel = path.relative(process.cwd(), full);
792
+ const rel = path.relative(process.cwd(), full).split(path.sep).join('/');
793
793
  if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return base;
794
794
  } catch {
795
795
  return base;
@@ -1112,7 +1112,8 @@ applyLegacyDeferredAcceptsOnStartup();
1112
1112
  restorePendingEventsFromStore();
1113
1113
  manualApply.pruneStaleEvidence();
1114
1114
  const portArg = args.find(a => a.startsWith('--port='));
1115
- state.port = portArg ? parseInt(portArg.split('=')[1], 10) : await findOpenPort();
1115
+ const explicitPort = portArg ? parseInt(portArg.split('=')[1], 10) : null;
1116
+ state.port = explicitPort ?? await findOpenPort();
1116
1117
  // Annotation screenshots live in the project root so the agent's Read tool
1117
1118
  // doesn't trip a per-file permission prompt. Sessioned by token so concurrent
1118
1119
  // projects (or quick restarts) don't collide.
@@ -1123,7 +1124,43 @@ state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-'));
1123
1124
  const { detectScript, liveScriptParts } = loadBrowserScripts();
1124
1125
  httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts }));
1125
1126
 
1127
+ // findOpenPort() probes a port, closes the probe socket, and only then does the
1128
+ // real server bind it. Another process scanning the same range can take the
1129
+ // port inside that gap, which surfaced as an intermittent
1130
+ // "EADDRINUSE 127.0.0.1:8405" — about one run in five of the test suite, where
1131
+ // several server-starting test files run in parallel.
1132
+ //
1133
+ // Retry on the next port when we picked it ourselves. An explicit --port is
1134
+ // never silently moved: the caller asked for that port, so a conflict there
1135
+ // must be an error they can see.
1136
+ const MAX_PORT_RETRIES = 25;
1137
+ let portRetries = 0;
1138
+
1139
+ httpServer.on('error', (err) => {
1140
+ if (err?.code !== 'EADDRINUSE') throw err;
1141
+
1142
+ if (explicitPort !== null) {
1143
+ console.error(`\nPort ${state.port} is already in use.`);
1144
+ console.error('Another live server may be running — stop it, or pass a different --port.');
1145
+ process.exit(1);
1146
+ }
1147
+
1148
+ if (portRetries >= MAX_PORT_RETRIES) {
1149
+ console.error(`\nCould not find a free port after ${MAX_PORT_RETRIES} attempts (last tried ${state.port}).`);
1150
+ process.exit(1);
1151
+ }
1152
+
1153
+ portRetries++;
1154
+ state.port++;
1155
+ httpServer.listen(state.port, '127.0.0.1');
1156
+ });
1157
+
1126
1158
  httpServer.listen(state.port, '127.0.0.1', () => {
1159
+ // Trust the address the OS actually bound over the one we asked for — after
1160
+ // a retry above, state.port and the bound port must not drift apart.
1161
+ const boundAddr = httpServer.address();
1162
+ if (boundAddr && typeof boundAddr === 'object') state.port = boundAddr.port;
1163
+
1127
1164
  writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token });
1128
1165
  const url = `http://localhost:${state.port}`;
1129
1166
  console.log(`\nMonodesign live server running on ${url}`);
@@ -102,7 +102,7 @@ The agent should insert variant HTML at insertLine.`);
102
102
  console.error(JSON.stringify({
103
103
  error: 'element_not_in_source',
104
104
  fallback: 'agent-driven',
105
- generatedMatch: path.relative(process.cwd(), generatedHit),
105
+ generatedMatch: path.relative(process.cwd(), generatedHit).split(path.sep).join('/'),
106
106
  hint: 'Element found only in a generated file. See "Handle fallback" in live.md.',
107
107
  }));
108
108
  } else {
@@ -119,7 +119,7 @@ The agent should insert variant HTML at insertLine.`);
119
119
  console.error(JSON.stringify({
120
120
  error: 'file_is_generated',
121
121
  fallback: 'agent-driven',
122
- file: path.relative(process.cwd(), path.resolve(process.cwd(), targetFile)),
122
+ file: path.relative(process.cwd(), path.resolve(process.cwd(), targetFile)).split(path.sep).join('/'),
123
123
  hint: 'Explicit --file points at a generated file. Writing here gets wiped by the next build. See "Handle fallback" in live.md.',
124
124
  }));
125
125
  process.exit(1);
@@ -172,7 +172,7 @@ The agent should insert variant HTML at insertLine.`);
172
172
  console.error(JSON.stringify({
173
173
  error: 'element_ambiguous',
174
174
  fallback: 'agent-driven',
175
- file: path.relative(process.cwd(), targetFile),
175
+ file: path.relative(process.cwd(), targetFile).split(path.sep).join('/'),
176
176
  candidates: filtered.map((c) => ({
177
177
  startLine: c.startLine + 1,
178
178
  endLine: c.endLine + 1,