0xray 4.0.32 → 4.0.33

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 (87) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/agents/api-design.yml +31 -0
  3. package/.opencode/agents/architect.yml +15 -0
  4. package/.opencode/agents/architecture-patterns.yml +14 -0
  5. package/.opencode/agents/auto-format.yml +14 -0
  6. package/.opencode/agents/backend-engineer.yml +12 -0
  7. package/.opencode/agents/boot-orchestrator.yml +14 -0
  8. package/.opencode/agents/bug-triage-specialist.yml +15 -0
  9. package/.opencode/agents/code-analyzer.yml +12 -0
  10. package/.opencode/agents/code-reviewer.yml +14 -0
  11. package/.opencode/agents/content-creator.yml +12 -0
  12. package/.opencode/agents/database-engineer.yml +12 -0
  13. package/.opencode/agents/devops-engineer.yml +12 -0
  14. package/.opencode/agents/framework-compliance-audit.yml +14 -0
  15. package/.opencode/agents/frontend-engineer.yml +12 -0
  16. package/.opencode/agents/frontend-ui-ux-engineer.yml +12 -0
  17. package/.opencode/agents/git-workflow.yml +15 -0
  18. package/.opencode/agents/growth-strategist.yml +12 -0
  19. package/.opencode/agents/hermes-agent.yml +12 -0
  20. package/.opencode/agents/inference-improve.yml +12 -0
  21. package/.opencode/agents/lint.yml +14 -0
  22. package/.opencode/agents/log-monitor.yml +12 -0
  23. package/.opencode/agents/mobile-developer.yml +12 -0
  24. package/.opencode/agents/model-health-check.yml +12 -0
  25. package/.opencode/agents/multimodal-looker.yml +12 -0
  26. package/.opencode/agents/performance-analysis.yml +12 -0
  27. package/.opencode/agents/performance-engineer.yml +12 -0
  28. package/.opencode/agents/performance-optimization.yml +12 -0
  29. package/.opencode/agents/processor-pipeline.yml +14 -0
  30. package/.opencode/agents/project-analysis.yml +12 -0
  31. package/.opencode/agents/refactorer.yml +12 -0
  32. package/.opencode/agents/researcher.yml +12 -0
  33. package/.opencode/agents/security-auditor.yml +12 -0
  34. package/.opencode/agents/security-scan.yml +12 -0
  35. package/.opencode/agents/seo-consultant.yml +12 -0
  36. package/.opencode/agents/session-management.yml +12 -0
  37. package/.opencode/agents/state-manager.yml +12 -0
  38. package/.opencode/agents/storyteller.yml +12 -0
  39. package/.opencode/agents/strategist.yml +12 -0
  40. package/.opencode/agents/tech-writer.yml +12 -0
  41. package/.opencode/agents/testing-best-practices.yml +12 -0
  42. package/.opencode/agents/testing-lead.yml +12 -0
  43. package/.opencode/agents/ui-ux-design.yml +12 -0
  44. package/.opencode/codex.codex +8 -0
  45. package/.opencode/commands/auto-format.md +99 -0
  46. package/.opencode/commands/auto-summary-capture.md +90 -0
  47. package/.opencode/commands/dependency-audit.md +184 -0
  48. package/.opencode/commands/enforcer-daily-scan.md +137 -0
  49. package/.opencode/commands/framework-compliance-audit.md +205 -0
  50. package/.opencode/commands/interactive-validator.md +75 -0
  51. package/.opencode/commands/job-summary-logger.md +68 -0
  52. package/.opencode/commands/lint.md +11 -0
  53. package/.opencode/commands/mode-switch.md +95 -0
  54. package/.opencode/commands/model-health-check.md +186 -0
  55. package/.opencode/commands/performance-analysis.md +144 -0
  56. package/.opencode/commands/pre-commit-introspection.md +185 -0
  57. package/.opencode/commands/pre-commit-introspection.sh +133 -0
  58. package/.opencode/commands/security-scan.md +157 -0
  59. package/.opencode/commands/sisyphus-validation.md +128 -0
  60. package/.opencode/commands/summary-logger.md +83 -0
  61. package/.opencode/enforcer-config.json +282 -0
  62. package/.opencode/skills/orchestrator/SKILL.md +3 -3
  63. package/.opencode/workflows/post-deployment-audit.yml +123 -0
  64. package/.xray/features.json +1 -1
  65. package/dist/CHANGELOG.md +8 -0
  66. package/dist/cli/goggles-status.js +1 -1
  67. package/dist/governance/governance-service.js +11 -1
  68. package/dist/integrations/grok/hooks/grok-hook-utils.js +7 -4
  69. package/dist/integrations/grok/hooks/pre-tool-use.js +7 -1
  70. package/dist/integrations/grok/hooks/session-start.js +5 -2
  71. package/dist/integrations/hooks/goggles-mcp.mjs +3 -36
  72. package/dist/integrations/hooks/goggles-pipeline.d.mts +2 -0
  73. package/dist/integrations/hooks/goggles-pipeline.mjs +393 -72
  74. package/dist/integrations/hooks/plates.cjs +15 -1
  75. package/dist/integrations/hooks/station-hook-runtime.cjs +53 -5
  76. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  77. package/dist/mcps/orchestrator/config/agent-capabilities.js +5 -0
  78. package/dist/nucleus/thin-dispatch.d.ts +4 -0
  79. package/dist/nucleus/thin-dispatch.js +9 -1
  80. package/dist/skills/orchestrator/SKILL.md +3 -3
  81. package/package.json +1 -1
  82. package/scripts/foundry/mint-suit.cjs +6 -1
  83. package/scripts/node/install-bridges.cjs +24 -7
  84. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  85. package/src/mcps/orchestrator/config/agent-capabilities.ts +4 -0
  86. package/src/skills/orchestrator/SKILL.md +3 -3
  87. package/xray/features.json +1 -1
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Goggles. Kind 0 name-checks the six outer names.
3
- * Digest and triage return one card. Empty stays empty.
3
+ * A named plane returns its view. Field pipes fill what they can see.
4
+ * The view keeps every field for the next look.
4
5
  * A held plane stays quiet. A leave is denied.
5
6
  */
6
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
7
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
8
+ import { createRequire } from 'node:module';
7
9
  import { dirname, join, resolve } from 'node:path';
8
10
  import { fileURLToPath } from 'node:url';
9
11
 
@@ -204,10 +206,20 @@ function gogglesAnswer(argv) {
204
206
  return { text: cards.map((card) => formatCardText(card)).join('\n\n'), cards };
205
207
  }
206
208
  const planes = acceptedPlanes().filter((name) => lower.includes(name));
207
- if (planes.length !== 1) return none('');
209
+ if (planes.length !== 1) {
210
+ if (!planes.length && cardPlanesNamed(lower).length) {
211
+ const cards = collectCards('digest', lower);
212
+ if (cards && cards.length) {
213
+ return { text: cards.map((card) => formatCardText(card)).join('\n\n'), cards };
214
+ }
215
+ }
216
+ return none('');
217
+ }
208
218
  const scopes = findScopes(words.filter((word) => word.toLowerCase() !== planes[0]));
209
219
  if (scopes.length > 1) return none('');
210
- return none(readingLine(planes[0], scopes[0] || ''));
220
+ const line = readingLine(planes[0], scopes[0] || '');
221
+ const file = moduleFile(planes[0]);
222
+ return none(file ? `${line} File: ${file}` : line);
211
223
  }
212
224
 
213
225
  export function readGoggles(argv) {
@@ -222,23 +234,17 @@ function cardPlanesNamed(lower) {
222
234
  return CARD_PLANES.filter((name) => lower.includes(name));
223
235
  }
224
236
 
225
- function listedFiles(plane, scope) {
226
- const files = Array.isArray(plane.files) ? plane.files.map(String) : [];
227
- if (scope === 'one artifact') return files.length === 1 ? [files[0]] : null;
228
- const unpathed = Array.isArray(plane.unpathed) ? plane.unpathed : [];
229
- const parts = files.slice();
230
- for (const name of unpathed) parts.push(name === 'mill' ? 'mill has no path' : String(name));
231
- return parts;
237
+ function listedFiles(plane) {
238
+ return Array.isArray(plane.files) ? plane.files.map(String) : [];
232
239
  }
233
240
 
234
241
  function cardView(plane, scope, flavor, root) {
235
- const listed = listedFiles(plane, scope);
236
- if (!listed) return null;
242
+ const listed = listedFiles(plane);
237
243
  const view = {
238
244
  plane: plane.id,
239
245
  flavor,
240
246
  scope: scope || '',
241
- from: plane.id === 'ground' ? '' : 'ground',
247
+ from: plane.id === 'ground' ? '' : (plane.from || 'ground'),
242
248
  digest: plane.digest || '',
243
249
  plate: plane.plate || '',
244
250
  entry: plane.entry || '',
@@ -248,7 +254,6 @@ function cardView(plane, scope, flavor, root) {
248
254
  setup: plane.setup || '',
249
255
  teardown: plane.teardown || '',
250
256
  worn: plane.worn || '',
251
- narrow: scope === 'one flow' || scope === 'one artifact',
252
257
  };
253
258
  if (flavor === 'triage') {
254
259
  const filled = new Set(filledOf(plane));
@@ -272,10 +277,7 @@ function formatCardText(view) {
272
277
  ['Teardown', view.teardown],
273
278
  ['Worn', view.worn],
274
279
  ];
275
- const shown = view.narrow
276
- ? rows.filter(([key]) => key === 'Plane' || key === 'Digest' || key === 'Files')
277
- : rows;
278
- let text = shown.map(([key, value]) => `${key}: ${value}`.trimEnd()).join('\n');
280
+ let text = rows.map(([key, value]) => `${key}: ${value}`.trimEnd()).join('\n');
279
281
  if (view.flavor === 'triage') {
280
282
  const lines = [];
281
283
  if (view.empty.length) lines.push(`Empty: ${view.empty.join(', ')}`);
@@ -286,23 +288,19 @@ function formatCardText(view) {
286
288
  }
287
289
 
288
290
  export function formatCardPane(view) {
289
- const rows = [['Plane', view.plane]];
290
- if (!view.narrow) rows.push(['From', view.from]);
291
- rows.push(['Digest', view.digest]);
292
- if (!view.narrow) {
293
- rows.push(
294
- ['Plate', view.plate],
295
- ['Entry', view.entry],
296
- ['Exit', view.exit],
297
- ['Files', view.files.join(', ')],
298
- ['Skills', view.skills],
299
- ['Setup', view.setup],
300
- ['Teardown', view.teardown],
301
- ['Worn', view.worn],
302
- );
303
- } else {
304
- rows.push(['Files', view.files.join(', ')]);
305
- }
291
+ const rows = [
292
+ ['Plane', view.plane],
293
+ ['From', view.from],
294
+ ['Digest', view.digest],
295
+ ['Plate', view.plate],
296
+ ['Entry', view.entry],
297
+ ['Exit', view.exit],
298
+ ['Files', view.files.join(', ')],
299
+ ['Skills', view.skills],
300
+ ['Setup', view.setup],
301
+ ['Teardown', view.teardown],
302
+ ['Worn', view.worn],
303
+ ];
306
304
  if (view.flavor === 'triage') {
307
305
  if (Array.isArray(view.empty) && view.empty.length) rows.push(['Empty', view.empty.join(', ')]);
308
306
  if (view.exam) rows.push(['', view.exam]);
@@ -319,20 +317,15 @@ export function formatCardPane(view) {
319
317
  function collectCards(flavor, lower) {
320
318
  const named = cardPlanesNamed(lower);
321
319
  const scopes = findScopes(lower.filter((word) => word !== flavor && !named.includes(word)));
322
- if (named.length > 1 || scopes.length > 1) return null;
320
+ if (scopes.length > 1) return null;
323
321
  const scope = scopes[0] || '';
324
- if (!named.length && (scope === 'part' || scope === 'one flow' || scope === 'one artifact')) return null;
325
322
  const platesDir = findPlatesDir(HERE);
326
- if (!platesDir) return null;
327
323
  const ids = named.length ? named : CARD_PLANES;
328
- const root = repoRootFrom(platesDir);
324
+ const root = sourceRoot(platesDir);
329
325
  const cards = [];
330
326
  for (const id of ids) {
331
- const plane = assemblePlane(id, platesDir);
332
- if (!plane) continue;
333
- const view = cardView(plane, scope, flavor, root);
334
- if (!view) return null;
335
- cards.push(view);
327
+ const plane = growPlane(id, platesDir, stateRoot());
328
+ cards.push(cardView(plane, scope, flavor, root));
336
329
  }
337
330
  return cards;
338
331
  }
@@ -366,6 +359,45 @@ function boxTitles(line) {
366
359
  return titles;
367
360
  }
368
361
 
362
+ function flowEnds(body) {
363
+ if (!body || body.includes('INPUT LAYER')) return null;
364
+ const stages = [];
365
+ let title = '';
366
+ let notes = [];
367
+ let open = false;
368
+ for (const line of String(body).split(/\r?\n/)) {
369
+ if (line.includes('┌')) {
370
+ open = true;
371
+ title = '';
372
+ notes = [];
373
+ continue;
374
+ }
375
+ if (!open) continue;
376
+ if (line.includes('└')) {
377
+ if (title) stages.push({ title, notes: notes.slice() });
378
+ open = false;
379
+ continue;
380
+ }
381
+ if (!/[A-Za-z]/.test(line)) continue;
382
+ const titles = boxTitles(line);
383
+ if (titles.length && !title) {
384
+ title = titles.join(' ');
385
+ continue;
386
+ }
387
+ const note = line.replace(/[│|]/g, ' ').replace(/^[\s·.-]+/, '').trim();
388
+ if (note) notes.push(note);
389
+ }
390
+ if (stages.length < 2) return null;
391
+ const first = stages[0];
392
+ const last = stages[stages.length - 1];
393
+ return {
394
+ entry: first.title,
395
+ exit: last.title,
396
+ setup: first.notes.join(' '),
397
+ teardown: last.notes.join(' '),
398
+ };
399
+ }
400
+
369
401
  function doorsOf(body) {
370
402
  let layer = '';
371
403
  let want = false;
@@ -433,40 +465,322 @@ export function filledOf(plane) {
433
465
  return FIELD_ORDER.filter((name) => flags[name]);
434
466
  }
435
467
 
468
+ function plateSource(id, platesDir) {
469
+ if (!platesDir || id === 'ground') return { body: '', path: '' };
470
+ const abs = join(platesDir, `${id}.md`);
471
+ if (!existsSync(abs)) return { body: '', path: '' };
472
+ return {
473
+ body: readFileSync(abs, 'utf8'),
474
+ path: join('docs-site', 'docs', 'plates', `${id}.md`),
475
+ };
476
+ }
477
+
478
+ function sectionOf(body, title) {
479
+ if (!body) return '';
480
+ const lines = stripFrontmatter(body).split(/\r?\n/);
481
+ const heading = new RegExp(`^#{1,3}\\s+${title}\\s*$`, 'i');
482
+ let on = false;
483
+ const prose = [];
484
+ for (const line of lines) {
485
+ if (heading.test(line)) {
486
+ on = true;
487
+ continue;
488
+ }
489
+ if (!on) continue;
490
+ if (/^#{1,3}\s+/.test(line) || line.startsWith('```')) break;
491
+ if (!line.trim()) {
492
+ if (prose.length) break;
493
+ continue;
494
+ }
495
+ prose.push(line.trim());
496
+ }
497
+ return prose.join(' ');
498
+ }
499
+
436
500
  export function assemblePlane(id, platesDir) {
437
501
  const extra = overlayTable()[id] || {};
438
- let plate = null;
439
- let digest = extra.digest || '';
440
- let doors = { entry: null, exit: null };
441
- if (id !== 'ground') {
442
- const abs = join(platesDir, `${id}.md`);
443
- if (!existsSync(abs)) return null;
444
- const raw = readFileSync(abs, 'utf8');
445
- plate = join('docs-site', 'docs', 'plates', `${id}.md`);
446
- if (!digest) digest = takeOf(stripFrontmatter(raw));
447
- doors = doorsOf(raw);
448
- }
449
- if (!digest) return null;
502
+ const src = plateSource(id, platesDir);
503
+ const doors = src.body ? doorsOf(src.body) : { entry: null, exit: null };
504
+ const flow = src.body ? flowEnds(src.body) : null;
505
+ const digest = extra.digest || (src.body ? takeOf(stripFrontmatter(src.body)) : '');
506
+ const files = Array.isArray(extra.files) ? extra.files.map(String) : [];
450
507
  return {
451
508
  id,
452
509
  digest,
453
- plate,
454
- entry: extra.entry || doors.entry || null,
455
- exit: extra.exit || doors.exit || null,
510
+ plate: src.path,
511
+ entry: extra.entry || doors.entry || (flow && flow.entry) || '',
512
+ exit: extra.exit || doors.exit || (flow && flow.exit) || '',
456
513
  entryIsMark: Boolean(extra.entry),
457
514
  exitIsMark: Boolean(extra.exit),
458
- files: extra.files || null,
515
+ files,
459
516
  unpathed: extra.unpathed || null,
460
- skills: extra.skills || null,
461
- setup: extra.setup || null,
462
- teardown: extra.teardown || null,
463
- worn: extra.worn || null,
517
+ skills: extra.skills || '',
518
+ setup: extra.setup || sectionOf(src.body, 'Setup') || (flow && flow.setup) || '',
519
+ teardown: extra.teardown || sectionOf(src.body, 'Teardown') || (flow && flow.teardown) || '',
520
+ worn: extra.worn || '',
521
+ clues: cluesOf(src.body),
464
522
  mark: extra.mark || null,
465
523
  wornMark: extra.wornMark || null,
466
524
  pick: extra.pick || null,
467
525
  };
468
526
  }
469
527
 
528
+ const VIEW_KEYS = ['from', 'digest', 'plate', 'entry', 'exit', 'files', 'skills', 'setup', 'teardown', 'worn'];
529
+
530
+ function stateRoot() {
531
+ const env = process.env.GOGGLES_ROOT;
532
+ if (typeof env === 'string' && env.trim() && env.trim() !== '.') return resolve(env.trim());
533
+ return process.cwd();
534
+ }
535
+
536
+ function sourceRoot(platesDir) {
537
+ return platesDir ? repoRootFrom(platesDir) : stateRoot();
538
+ }
539
+
540
+ function viewsPath(root) {
541
+ return join(root, '.xray', 'state', 'goggles-views.json');
542
+ }
543
+
544
+ function loadViews(root) {
545
+ try {
546
+ const data = JSON.parse(readFileSync(viewsPath(root), 'utf8'));
547
+ return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
548
+ } catch {
549
+ return {};
550
+ }
551
+ }
552
+
553
+ function saveViews(root, views) {
554
+ const file = viewsPath(root);
555
+ mkdirSync(dirname(file), { recursive: true });
556
+ writeFileSync(file, `${JSON.stringify(views, null, 2)}\n`);
557
+ }
558
+
559
+ function cluesOf(body) {
560
+ if (!body) return [];
561
+ const clues = [];
562
+ for (const match of String(body).matchAll(/`([^`]{4,80})`/g)) clues.push(match[1]);
563
+ for (const line of String(body).split(/\r?\n/)) {
564
+ if (!line.includes('│')) continue;
565
+ for (const title of boxTitles(line)) {
566
+ if (title.length >= 8 && title.length <= 60) clues.push(title);
567
+ }
568
+ }
569
+ return clues;
570
+ }
571
+
572
+ const sourceCache = new Map();
573
+
574
+ function sourceTexts(root) {
575
+ if (sourceCache.has(root)) return sourceCache.get(root);
576
+ const out = [];
577
+ const walk = (dir, depth) => {
578
+ if (depth > 8) return;
579
+ let entries = [];
580
+ try {
581
+ entries = readdirSync(dir, { withFileTypes: true });
582
+ } catch {
583
+ return;
584
+ }
585
+ for (const entry of entries) {
586
+ if (entry.name === 'node_modules' || entry.name === '__tests__' || entry.name === 'dist') continue;
587
+ const abs = join(dir, entry.name);
588
+ if (entry.isDirectory()) {
589
+ walk(abs, depth + 1);
590
+ continue;
591
+ }
592
+ if (!/\.(ts|mjs|cjs)$/.test(entry.name) || entry.name.endsWith('.test.ts')) continue;
593
+ try {
594
+ out.push({ rel: abs.slice(root.length + 1), text: readFileSync(abs, 'utf8') });
595
+ } catch {
596
+ /* unreadable source is not a plane file */
597
+ }
598
+ }
599
+ };
600
+ walk(join(root, 'src'), 0);
601
+ sourceCache.set(root, out);
602
+ return out;
603
+ }
604
+
605
+ function fileFromClues(clues, root, planeId) {
606
+ const texts = sourceTexts(root);
607
+ const votes = new Map();
608
+ for (const clue of clues) {
609
+ const hits = [];
610
+ for (const file of texts) {
611
+ if (!file.text.includes(clue)) continue;
612
+ hits.push(file.rel);
613
+ if (hits.length > 8) break;
614
+ }
615
+ if (!hits.length || hits.length > 8) continue;
616
+ const weight = 9 - hits.length;
617
+ for (const rel of hits) votes.set(rel, (votes.get(rel) || 0) + weight);
618
+ }
619
+ const token = String(planeId || '').split('-')[0].toLowerCase();
620
+ const named = token ? [...votes.keys()].filter((rel) => rel.toLowerCase().includes(token)) : [];
621
+ const pool = new Set(named.length ? named : [...votes.keys()]);
622
+ let best = '';
623
+ let bestRank = -1;
624
+ for (const [rel, score] of votes) {
625
+ if (!pool.has(rel)) continue;
626
+ let rank = score * 10;
627
+ if (token && rel.toLowerCase().includes(token)) rank += 20;
628
+ if (rel.startsWith('src/cli/')) rank -= 3;
629
+ if (rank > bestRank || (rank === bestRank && rel.length < best.length)) {
630
+ best = rel;
631
+ bestRank = rank;
632
+ }
633
+ }
634
+ return best;
635
+ }
636
+
637
+ function skillFor(id, root) {
638
+ const dir = join(root, 'src', 'skills');
639
+ if (!existsSync(dir)) return '';
640
+ let names = [];
641
+ try {
642
+ names = readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
643
+ } catch {
644
+ return '';
645
+ }
646
+ const hit = names.filter((name) => name === id || name.startsWith(`${id}-`));
647
+ if (hit.length !== 1) return '';
648
+ const rel = join('src', 'skills', hit[0], 'SKILL.md');
649
+ return existsSync(join(root, rel)) ? rel : '';
650
+ }
651
+
652
+ function moduleFile(name) {
653
+ const platesDir = findPlatesDir(HERE);
654
+ const root = sourceRoot(platesDir);
655
+ const hit = sourceTexts(root).find((file) => file.rel.endsWith(`/${name}.ts`) || file.rel.endsWith(`/${name}.mjs`));
656
+ return hit ? hit.rel : '';
657
+ }
658
+
659
+ function hintLine(name) {
660
+ const cards = lookCards(['digest', name]);
661
+ const files = cards && cards[0] && Array.isArray(cards[0].files) ? cards[0].files : [];
662
+ return files.length ? `${name}: ${files.join(', ')}` : '';
663
+ }
664
+
665
+ export function suitHint(text) {
666
+ const words = intentWords(text);
667
+ const named = CARD_PLANES.filter((name) => words.includes(name));
668
+ if (!named.length) return lookVerb(words) ? 'Name one plane.' : '';
669
+ return named.map(hintLine).filter(Boolean).join('\n');
670
+ }
671
+
672
+ const DEEP_SEARCH = /search_codebase|find_implementation|get_documentation/i;
673
+
674
+ function researchCall(toolName, text) {
675
+ const tool = String(toolName || '');
676
+ const spoken = String(text || '');
677
+ if (/researcher|explorer|deep[- ]?research/i.test(tool) || DEEP_SEARCH.test(tool)) return true;
678
+ return /bash|shell/i.test(tool) && (/researcher|explorer|deep[- ]?research/i.test(spoken) || DEEP_SEARCH.test(spoken));
679
+ }
680
+
681
+ /** One plane: the files the suit continues with. A look with no plane stops. Two planes stay quiet. */
682
+ export function lensPlane(text) {
683
+ const hint = suitHint(String(text || ''));
684
+ if (hint === 'Name one plane.') return { stop: true, files: [] };
685
+ if (!hint || hint.includes('\n')) return { stop: false, files: [] };
686
+ const mark = hint.indexOf(': ');
687
+ if (mark < 0) return { stop: false, files: [] };
688
+ const files = hint.slice(mark + 2).split(',').map((part) => part.trim()).filter(Boolean);
689
+ return { stop: false, files };
690
+ }
691
+
692
+ function lensPage(root, names) {
693
+ if (!names.length) return 'Name one plane.';
694
+ const platesDir = findPlatesDir(HERE);
695
+ const parts = [];
696
+ for (const id of names) {
697
+ const plane = growPlane(id, platesDir, root);
698
+ parts.push(formatCardText(cardView(plane, '', 'digest', sourceRoot(platesDir))));
699
+ }
700
+ return parts.join('\n\n');
701
+ }
702
+
703
+ function stampPlane(root, id) {
704
+ try {
705
+ const plates = createRequire(import.meta.url)('./plates.cjs');
706
+ if (!plates.PLATE_IDS.includes(id)) return;
707
+ plates.stampPlateIfMissing(root, id);
708
+ } catch {
709
+ /* a missed stamp must not fail the stop */
710
+ }
711
+ }
712
+
713
+ /** A search stays stopped. The next move is the file on the lens, not the same search again. */
714
+ export function lensBeforeResearch(root, toolName, text) {
715
+ const spoken = String(text || '');
716
+ if (!researchCall(toolName, spoken)) return null;
717
+ const names = CARD_PLANES.filter((name) => intentWords(spoken).includes(name));
718
+ if (names.length === 1) stampPlane(root, names[0]);
719
+ return { gate: 'lens', decision: 'deny', reason: lensPage(root, names) };
720
+ }
721
+
722
+ function seenFields(plane, root) {
723
+ const files = Array.isArray(plane.files) ? plane.files.slice() : [];
724
+ let worn = plane.worn || '';
725
+ if (!files.length && Array.isArray(plane.unpathed) && plane.unpathed.includes('mill')) {
726
+ if (existsSync(join(root, 'scripts', 'foundry'))) files.push('scripts/foundry');
727
+ }
728
+ if (!files.length && Array.isArray(plane.clues) && plane.clues.length) {
729
+ const found = fileFromClues(plane.clues, root, plane.id);
730
+ if (found) files.push(found);
731
+ }
732
+ let skills = plane.skills || '';
733
+ if (!skills) skills = skillFor(plane.id, root);
734
+ if (!worn) {
735
+ for (const file of files) {
736
+ if (!file.startsWith('src/')) continue;
737
+ let rel = file.slice(4);
738
+ if (rel.endsWith('.ts')) rel = rel.replace(/\.ts$/, '.js');
739
+ else if (!/\.(cjs|mjs|js)$/.test(rel)) continue;
740
+ const next = `dist/${rel}`;
741
+ if (existsSync(join(root, next))) {
742
+ worn = next;
743
+ break;
744
+ }
745
+ }
746
+ }
747
+ return {
748
+ from: plane.id === 'ground' ? '' : 'ground',
749
+ digest: plane.digest || '',
750
+ plate: plane.plate || '',
751
+ entry: plane.entry || '',
752
+ exit: plane.exit || '',
753
+ files,
754
+ skills,
755
+ setup: plane.setup || '',
756
+ teardown: plane.teardown || '',
757
+ worn,
758
+ };
759
+ }
760
+
761
+ function kept(stored, seen, key) {
762
+ if (key === 'files') {
763
+ if (Array.isArray(seen.files) && seen.files.length) return seen.files.map(String);
764
+ return Array.isArray(stored.files) ? stored.files.map(String) : [];
765
+ }
766
+ if (seen[key]) return seen[key];
767
+ return stored[key] || '';
768
+ }
769
+
770
+ export function growPlane(id, platesDir, root) {
771
+ const seen = seenFields(assemblePlane(id, platesDir), sourceRoot(platesDir));
772
+ const views = loadViews(root);
773
+ const stored = views[id] && typeof views[id] === 'object' ? views[id] : {};
774
+ const grown = { id };
775
+ for (const key of VIEW_KEYS) grown[key] = kept(stored, seen, key);
776
+ if (id === 'ground') grown.from = stored.from || '';
777
+ views[id] = {};
778
+ for (const key of VIEW_KEYS) views[id][key] = grown[key];
779
+ saveViews(root, views);
780
+ const base = assemblePlane(id, platesDir);
781
+ return { ...base, ...grown };
782
+ }
783
+
470
784
  function repoRootFrom(platesDir) {
471
785
  return dirname(dirname(dirname(platesDir)));
472
786
  }
@@ -535,8 +849,8 @@ function isGogglesWork(text) {
535
849
 
536
850
 
537
851
  export function cardStop(root, toolName, text, paths = []) {
538
- const raw = [String(text || ''), ...(paths || [])].join('\n');
539
- if (isGogglesWork(raw)) {
852
+ const spoken = String(text || '');
853
+ if (spoken.length <= 200 && isGogglesWork(spoken) && !(paths || []).some((item) => /goggles/i.test(String(item)))) {
540
854
  clearReading(root);
541
855
  clearCardLine(root);
542
856
  return null;
@@ -574,15 +888,21 @@ function intentWords(text) {
574
888
  return String(text || '').toLowerCase().split(/[^a-z0-9-]+/).filter(Boolean);
575
889
  }
576
890
 
891
+ function lookVerb(words) {
892
+ return words.includes('look') || words.includes('name');
893
+ }
894
+
577
895
  export function handCard(root, intent) {
578
896
  try {
579
- clearCardLine(root);
580
897
  const text = String(intent || '');
581
- if (isGogglesWork(text)) {
898
+ const words = intentWords(text);
899
+ if (isGogglesWork(text) && text.length <= 200) {
582
900
  clearReading(root);
901
+ clearCardLine(root);
583
902
  return null;
584
903
  }
585
- const words = intentWords(text);
904
+ if (!lookVerb(words)) return null;
905
+ clearCardLine(root);
586
906
  const looked = readGoggles(words);
587
907
  if (looked.text.startsWith('Actuality. Drift')) {
588
908
  saveReading(root, { drift: looked.text });
@@ -615,10 +935,11 @@ export function lensPath(root) {
615
935
 
616
936
 
617
937
  export function maintainLens(root) {
618
- const text = actualityLine();
938
+ const drift = actualityLine();
939
+ const text = drift || 'quiet (match)';
619
940
  mkdirSync(dirname(lensPath(root)), { recursive: true });
620
941
  writeFileSync(lensPath(root), `${text}\n`);
621
- return ok(text);
942
+ return ok(drift);
622
943
  }
623
944
 
624
945
 
@@ -179,10 +179,24 @@ function stampPlateIfMissing(projectRoot, id) {
179
179
  return { id, path, written: true };
180
180
  }
181
181
 
182
+ function organFile(intent) {
183
+ try {
184
+ const { suitHint } = require("./goggles-pipeline.mjs");
185
+ const hint = suitHint(String(intent || ""));
186
+ if (!hint || hint === "Name one plane." || hint.includes("\n")) return "";
187
+ const file = hint.split(": ").slice(1).join(": ").trim();
188
+ return file;
189
+ } catch {
190
+ return "";
191
+ }
192
+ }
193
+
182
194
  function plateStockLine(intent) {
183
195
  const plate = recallPlate(intent);
184
196
  if (!plate) return null;
185
- return `Plate: ${plate.id} — .xray/state/plates/${plate.id}.md`;
197
+ const line = `Plate: ${plate.id} — .xray/state/plates/${plate.id}.md`;
198
+ const file = organFile(intent);
199
+ return file ? `${line} File: ${file}` : line;
186
200
  }
187
201
 
188
202
  module.exports = {