0xray 4.0.31 → 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 (98) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.mcp.json +8 -0
  3. package/.opencode/agents/api-design.yml +31 -0
  4. package/.opencode/agents/architect.yml +15 -0
  5. package/.opencode/agents/architecture-patterns.yml +14 -0
  6. package/.opencode/agents/auto-format.yml +14 -0
  7. package/.opencode/agents/backend-engineer.yml +12 -0
  8. package/.opencode/agents/boot-orchestrator.yml +14 -0
  9. package/.opencode/agents/bug-triage-specialist.yml +15 -0
  10. package/.opencode/agents/code-analyzer.yml +12 -0
  11. package/.opencode/agents/code-reviewer.yml +14 -0
  12. package/.opencode/agents/content-creator.yml +12 -0
  13. package/.opencode/agents/database-engineer.yml +12 -0
  14. package/.opencode/agents/devops-engineer.yml +12 -0
  15. package/.opencode/agents/framework-compliance-audit.yml +14 -0
  16. package/.opencode/agents/frontend-engineer.yml +12 -0
  17. package/.opencode/agents/frontend-ui-ux-engineer.yml +12 -0
  18. package/.opencode/agents/git-workflow.yml +15 -0
  19. package/.opencode/agents/growth-strategist.yml +12 -0
  20. package/.opencode/agents/hermes-agent.yml +12 -0
  21. package/.opencode/agents/inference-improve.yml +12 -0
  22. package/.opencode/agents/lint.yml +14 -0
  23. package/.opencode/agents/log-monitor.yml +12 -0
  24. package/.opencode/agents/mobile-developer.yml +12 -0
  25. package/.opencode/agents/model-health-check.yml +12 -0
  26. package/.opencode/agents/multimodal-looker.yml +12 -0
  27. package/.opencode/agents/performance-analysis.yml +12 -0
  28. package/.opencode/agents/performance-engineer.yml +12 -0
  29. package/.opencode/agents/performance-optimization.yml +12 -0
  30. package/.opencode/agents/processor-pipeline.yml +14 -0
  31. package/.opencode/agents/project-analysis.yml +12 -0
  32. package/.opencode/agents/refactorer.yml +12 -0
  33. package/.opencode/agents/researcher.yml +12 -0
  34. package/.opencode/agents/security-auditor.yml +12 -0
  35. package/.opencode/agents/security-scan.yml +12 -0
  36. package/.opencode/agents/seo-consultant.yml +12 -0
  37. package/.opencode/agents/session-management.yml +12 -0
  38. package/.opencode/agents/state-manager.yml +12 -0
  39. package/.opencode/agents/storyteller.yml +12 -0
  40. package/.opencode/agents/strategist.yml +12 -0
  41. package/.opencode/agents/tech-writer.yml +12 -0
  42. package/.opencode/agents/testing-best-practices.yml +12 -0
  43. package/.opencode/agents/testing-lead.yml +12 -0
  44. package/.opencode/agents/ui-ux-design.yml +12 -0
  45. package/.opencode/codex.codex +8 -0
  46. package/.opencode/commands/auto-format.md +99 -0
  47. package/.opencode/commands/auto-summary-capture.md +90 -0
  48. package/.opencode/commands/dependency-audit.md +184 -0
  49. package/.opencode/commands/enforcer-daily-scan.md +137 -0
  50. package/.opencode/commands/framework-compliance-audit.md +205 -0
  51. package/.opencode/commands/interactive-validator.md +75 -0
  52. package/.opencode/commands/job-summary-logger.md +68 -0
  53. package/.opencode/commands/lint.md +11 -0
  54. package/.opencode/commands/mode-switch.md +95 -0
  55. package/.opencode/commands/model-health-check.md +186 -0
  56. package/.opencode/commands/performance-analysis.md +144 -0
  57. package/.opencode/commands/pre-commit-introspection.md +185 -0
  58. package/.opencode/commands/pre-commit-introspection.sh +133 -0
  59. package/.opencode/commands/security-scan.md +157 -0
  60. package/.opencode/commands/sisyphus-validation.md +128 -0
  61. package/.opencode/commands/summary-logger.md +83 -0
  62. package/.opencode/enforcer-config.json +282 -0
  63. package/.opencode/skills/orchestrator/SKILL.md +5 -5
  64. package/.opencode/workflows/post-deployment-audit.yml +123 -0
  65. package/.xray/features.json +1 -1
  66. package/dist/CHANGELOG.md +16 -0
  67. package/dist/cli/commands/status.d.ts +4 -2
  68. package/dist/cli/commands/status.js +6 -3
  69. package/dist/cli/commands/validate.js +4 -0
  70. package/dist/cli/goggles-status.d.ts +13 -0
  71. package/dist/cli/goggles-status.js +76 -0
  72. package/dist/cli/index.js +31 -7
  73. package/dist/governance/governance-service.js +11 -1
  74. package/dist/integrations/grok/grok-cli.d.ts +3 -0
  75. package/dist/integrations/grok/grok-cli.js +41 -1
  76. package/dist/integrations/grok/hooks/grok-hook-utils.js +7 -4
  77. package/dist/integrations/grok/hooks/pre-tool-use.js +7 -1
  78. package/dist/integrations/grok/hooks/session-start.js +5 -2
  79. package/dist/integrations/hooks/goggles-mcp.mjs +338 -0
  80. package/dist/integrations/hooks/goggles-pipeline.d.mts +2 -0
  81. package/dist/integrations/hooks/goggles-pipeline.mjs +485 -100
  82. package/dist/integrations/hooks/plates.cjs +15 -1
  83. package/dist/integrations/hooks/station-hook-runtime.cjs +53 -5
  84. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  85. package/dist/mcps/orchestrator/config/agent-capabilities.js +5 -0
  86. package/dist/nucleus/thin-dispatch.d.ts +4 -0
  87. package/dist/nucleus/thin-dispatch.js +9 -1
  88. package/dist/skills/orchestrator/SKILL.md +5 -5
  89. package/package.json +2 -1
  90. package/scripts/foundry/assert-packed-dist-cli.mjs +4 -0
  91. package/scripts/foundry/mint-suit.cjs +6 -1
  92. package/scripts/mjs/run-goggles-mcp.mjs +41 -0
  93. package/scripts/node/bridge-mcp-wiring.cjs +61 -21
  94. package/scripts/node/install-bridges.cjs +24 -7
  95. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  96. package/src/mcps/orchestrator/config/agent-capabilities.ts +4 -0
  97. package/src/skills/orchestrator/SKILL.md +5 -5
  98. 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
 
@@ -14,9 +16,16 @@ const CARD_PLANES = ['ground', 'routing', 'house', 'boot', 'governance', 'memory
14
16
  const CARD_FLAVORS = ['digest', 'triage'];
15
17
  const SCOPE_ZOOM = ['ecosystem', 'part', 'one flow', 'one artifact'];
16
18
 
19
+ /** Empty GOGGLES_PLANES_PATH keeps the file beside the organ. */
20
+ export function planesFile() {
21
+ const override = process.env.GOGGLES_PLANES_PATH;
22
+ if (typeof override === 'string' && override.trim()) return override.trim();
23
+ return join(HERE, 'goggles-planes.json');
24
+ }
25
+
17
26
  function drawnPlanes() {
18
27
  try {
19
- const data = JSON.parse(readFileSync(join(HERE, 'goggles-planes.json'), 'utf8'));
28
+ const data = JSON.parse(readFileSync(planesFile(), 'utf8'));
20
29
  return Array.isArray(data.planes) ? data.planes.map((name) => String(name)) : [];
21
30
  } catch {
22
31
  return [];
@@ -169,7 +178,7 @@ function namesTheSet(lower) {
169
178
  return planes.length === 1 && planes[0] === 'loop' && findScopes(lower).length === 0;
170
179
  }
171
180
 
172
- export function readGoggles(argv) {
181
+ function gogglesAnswer(argv) {
173
182
  const words = [];
174
183
  for (const raw of argv || []) {
175
184
  const arg = String(raw || '').trim();
@@ -177,102 +186,148 @@ export function readGoggles(argv) {
177
186
  words.push(arg);
178
187
  }
179
188
  const lower = words.map((word) => word.toLowerCase());
180
- if (lower.includes('calling')) return ok('');
181
- if (kindPastOne(words)) return ok('');
182
- if (words.length === 0) return ok('');
183
- if (namesTheSet(lower)) return ok('Name one plane.');
189
+ const none = (text) => ({ text, cards: null });
190
+ if (lower.includes('calling')) return none('');
191
+ if (kindPastOne(words)) return none('');
192
+ if (words.length === 0) return none('');
193
+ if (namesTheSet(lower)) return none('Name one plane.');
184
194
  if (isKindZero(lower)) {
185
- if (findScopes(words).length) return ok('');
186
- if (acceptedPlanes().some((name) => lower.includes(name))) return ok('');
187
- return ok(actualityLine());
195
+ if (findScopes(words).length) return none('');
196
+ if (acceptedPlanes().some((name) => lower.includes(name))) return none('');
197
+ return none(actualityLine());
188
198
  }
189
199
  const flavors = CARD_FLAVORS.filter((name) => lower.includes(name));
190
- if (flavors.length > 1) return ok('');
200
+ if (flavors.length > 1) return none('');
191
201
  if (flavors.length === 1) {
192
202
  const others = acceptedPlanes().filter((name) => name !== flavors[0] && lower.includes(name));
193
- if (others.length) return ok('');
194
- return ok(cardLook(flavors[0], lower));
203
+ if (others.length) return none('');
204
+ const cards = collectCards(flavors[0], lower);
205
+ if (!cards) return none('');
206
+ return { text: cards.map((card) => formatCardText(card)).join('\n\n'), cards };
195
207
  }
196
208
  const planes = acceptedPlanes().filter((name) => lower.includes(name));
197
- if (planes.length !== 1) return ok('');
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
+ }
198
218
  const scopes = findScopes(words.filter((word) => word.toLowerCase() !== planes[0]));
199
- if (scopes.length > 1) return ok('');
200
- return ok(readingLine(planes[0], scopes[0] || ''));
219
+ if (scopes.length > 1) return none('');
220
+ const line = readingLine(planes[0], scopes[0] || '');
221
+ const file = moduleFile(planes[0]);
222
+ return none(file ? `${line} File: ${file}` : line);
223
+ }
224
+
225
+ export function readGoggles(argv) {
226
+ return ok(gogglesAnswer(argv).text);
227
+ }
228
+
229
+ export function lookCards(argv) {
230
+ return gogglesAnswer(argv).cards;
201
231
  }
202
232
 
203
233
  function cardPlanesNamed(lower) {
204
234
  return CARD_PLANES.filter((name) => lower.includes(name));
205
235
  }
206
236
 
207
- function fileList(plane) {
208
- const files = Array.isArray(plane.files) ? plane.files : [];
209
- const unpathed = Array.isArray(plane.unpathed) ? plane.unpathed : [];
210
- const parts = [...files];
211
- for (const name of unpathed) {
212
- parts.push(name === 'mill' ? 'mill has no path' : name);
237
+ function listedFiles(plane) {
238
+ return Array.isArray(plane.files) ? plane.files.map(String) : [];
239
+ }
240
+
241
+ function cardView(plane, scope, flavor, root) {
242
+ const listed = listedFiles(plane);
243
+ const view = {
244
+ plane: plane.id,
245
+ flavor,
246
+ scope: scope || '',
247
+ from: plane.id === 'ground' ? '' : (plane.from || 'ground'),
248
+ digest: plane.digest || '',
249
+ plate: plane.plate || '',
250
+ entry: plane.entry || '',
251
+ exit: plane.exit || '',
252
+ files: listed,
253
+ skills: plane.skills || '',
254
+ setup: plane.setup || '',
255
+ teardown: plane.teardown || '',
256
+ worn: plane.worn || '',
257
+ };
258
+ if (flavor === 'triage') {
259
+ const filled = new Set(filledOf(plane));
260
+ view.empty = FIELD_ORDER.filter((name) => !filled.has(name));
261
+ view.exam = examinePlane(plane, root).text;
213
262
  }
214
- return parts.join(', ');
263
+ return view;
215
264
  }
216
265
 
217
- function cardRows(plane, scope) {
218
- const files = Array.isArray(plane.files) ? plane.files : [];
219
- if (scope === 'one artifact' && files.length !== 1) return null;
266
+ function formatCardText(view) {
220
267
  const rows = [
221
- ['Plane', plane.id],
222
- ['From', plane.id === 'ground' ? '' : 'ground'],
223
- ['Digest', plane.digest || ''],
224
- ['Plate', plane.plate || ''],
225
- ['Entry', plane.entry || ''],
226
- ['Exit', plane.exit || ''],
227
- ['Files', scope === 'one artifact' ? files[0] : fileList(plane)],
228
- ['Skills', plane.skills || ''],
229
- ['Setup', plane.setup || ''],
230
- ['Teardown', plane.teardown || ''],
231
- ['Worn', plane.worn || ''],
268
+ ['Plane', view.plane],
269
+ ['From', view.from],
270
+ ['Digest', view.digest],
271
+ ['Plate', view.plate],
272
+ ['Entry', view.entry],
273
+ ['Exit', view.exit],
274
+ ['Files', view.files.join(', ')],
275
+ ['Skills', view.skills],
276
+ ['Setup', view.setup],
277
+ ['Teardown', view.teardown],
278
+ ['Worn', view.worn],
232
279
  ];
233
- if (scope === 'one flow' || scope === 'one artifact') {
234
- return rows.filter(([key]) => key === 'Plane' || key === 'Digest' || key === 'Files');
280
+ let text = rows.map(([key, value]) => `${key}: ${value}`.trimEnd()).join('\n');
281
+ if (view.flavor === 'triage') {
282
+ const lines = [];
283
+ if (view.empty.length) lines.push(`Empty: ${view.empty.join(', ')}`);
284
+ lines.push(view.exam);
285
+ text = `${text}\n${lines.join('\n')}`;
235
286
  }
236
- return rows;
237
- }
238
-
239
- function triageNote(plane, root) {
240
- const filled = new Set(filledOf(plane));
241
- const empty = FIELD_ORDER.filter((name) => !filled.has(name));
242
- const exam = examinePlane(plane, root);
243
- const lines = [];
244
- if (empty.length) lines.push(`Empty: ${empty.join(', ')}`);
245
- lines.push(exam.text);
246
- return lines.join('\n');
287
+ return text;
247
288
  }
248
289
 
249
- function formatPlaneCard(plane, scope, flavor, root) {
250
- const rows = cardRows(plane, scope);
251
- if (!rows) return null;
252
- let text = rows.map(([key, value]) => `${key}: ${value}`.trimEnd()).join('\n');
253
- if (flavor === 'triage') text = `${text}\n${triageNote(plane, root)}`;
254
- return text;
290
+ export function formatCardPane(view) {
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
+ ];
304
+ if (view.flavor === 'triage') {
305
+ if (Array.isArray(view.empty) && view.empty.length) rows.push(['Empty', view.empty.join(', ')]);
306
+ if (view.exam) rows.push(['', view.exam]);
307
+ }
308
+ const body = rows.map(([label, value]) => (label ? `${label.padEnd(8)} ${value}` : value).trimEnd());
309
+ const title = `${view.flavor} · ${view.plane}`;
310
+ const width = Math.max(title.length + 2, ...body.map((line) => line.length));
311
+ const top = `┌ ${title} ${'─'.repeat(Math.max(0, width - title.length - 1))}┐`;
312
+ const mid = body.map((line) => `│ ${line.padEnd(width)} │`);
313
+ const bot = `└${'─'.repeat(width + 2)}┘`;
314
+ return [top, ...mid, bot].join('\n');
255
315
  }
256
316
 
257
- function cardLook(flavor, lower) {
317
+ function collectCards(flavor, lower) {
258
318
  const named = cardPlanesNamed(lower);
259
319
  const scopes = findScopes(lower.filter((word) => word !== flavor && !named.includes(word)));
260
- if (named.length > 1 || scopes.length > 1) return '';
320
+ if (scopes.length > 1) return null;
261
321
  const scope = scopes[0] || '';
262
- if (!named.length && (scope === 'part' || scope === 'one flow' || scope === 'one artifact')) return '';
263
322
  const platesDir = findPlatesDir(HERE);
264
- if (!platesDir) return '';
265
323
  const ids = named.length ? named : CARD_PLANES;
266
- const root = repoRootFrom(platesDir);
267
- const blocks = [];
324
+ const root = sourceRoot(platesDir);
325
+ const cards = [];
268
326
  for (const id of ids) {
269
- const plane = assemblePlane(id, platesDir);
270
- if (!plane) continue;
271
- const text = formatPlaneCard(plane, scope, flavor, root);
272
- if (text === null) return '';
273
- blocks.push(text);
327
+ const plane = growPlane(id, platesDir, stateRoot());
328
+ cards.push(cardView(plane, scope, flavor, root));
274
329
  }
275
- return blocks.join('\n\n');
330
+ return cards;
276
331
  }
277
332
 
278
333
  export function findPlatesDir(start) {
@@ -304,6 +359,45 @@ function boxTitles(line) {
304
359
  return titles;
305
360
  }
306
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
+
307
401
  function doorsOf(body) {
308
402
  let layer = '';
309
403
  let want = false;
@@ -347,7 +441,7 @@ function takeOf(body) {
347
441
 
348
442
 
349
443
  function overlayTable() {
350
- const file = join(HERE, 'goggles-planes.json');
444
+ const file = planesFile();
351
445
  if (!existsSync(file)) return {};
352
446
  return JSON.parse(readFileSync(file, 'utf8'));
353
447
  }
@@ -371,40 +465,322 @@ export function filledOf(plane) {
371
465
  return FIELD_ORDER.filter((name) => flags[name]);
372
466
  }
373
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
+
374
500
  export function assemblePlane(id, platesDir) {
375
501
  const extra = overlayTable()[id] || {};
376
- let plate = null;
377
- let digest = extra.digest || '';
378
- let doors = { entry: null, exit: null };
379
- if (id !== 'ground') {
380
- const abs = join(platesDir, `${id}.md`);
381
- if (!existsSync(abs)) return null;
382
- const raw = readFileSync(abs, 'utf8');
383
- plate = join('docs-site', 'docs', 'plates', `${id}.md`);
384
- if (!digest) digest = takeOf(stripFrontmatter(raw));
385
- doors = doorsOf(raw);
386
- }
387
- 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) : [];
388
507
  return {
389
508
  id,
390
509
  digest,
391
- plate,
392
- entry: extra.entry || doors.entry || null,
393
- 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) || '',
394
513
  entryIsMark: Boolean(extra.entry),
395
514
  exitIsMark: Boolean(extra.exit),
396
- files: extra.files || null,
515
+ files,
397
516
  unpathed: extra.unpathed || null,
398
- skills: extra.skills || null,
399
- setup: extra.setup || null,
400
- teardown: extra.teardown || null,
401
- 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),
402
522
  mark: extra.mark || null,
403
523
  wornMark: extra.wornMark || null,
404
524
  pick: extra.pick || null,
405
525
  };
406
526
  }
407
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
+
408
784
  function repoRootFrom(platesDir) {
409
785
  return dirname(dirname(dirname(platesDir)));
410
786
  }
@@ -473,8 +849,8 @@ function isGogglesWork(text) {
473
849
 
474
850
 
475
851
  export function cardStop(root, toolName, text, paths = []) {
476
- const raw = [String(text || ''), ...(paths || [])].join('\n');
477
- if (isGogglesWork(raw)) {
852
+ const spoken = String(text || '');
853
+ if (spoken.length <= 200 && isGogglesWork(spoken) && !(paths || []).some((item) => /goggles/i.test(String(item)))) {
478
854
  clearReading(root);
479
855
  clearCardLine(root);
480
856
  return null;
@@ -512,15 +888,21 @@ function intentWords(text) {
512
888
  return String(text || '').toLowerCase().split(/[^a-z0-9-]+/).filter(Boolean);
513
889
  }
514
890
 
891
+ function lookVerb(words) {
892
+ return words.includes('look') || words.includes('name');
893
+ }
894
+
515
895
  export function handCard(root, intent) {
516
896
  try {
517
- clearCardLine(root);
518
897
  const text = String(intent || '');
519
- if (isGogglesWork(text)) {
898
+ const words = intentWords(text);
899
+ if (isGogglesWork(text) && text.length <= 200) {
520
900
  clearReading(root);
901
+ clearCardLine(root);
521
902
  return null;
522
903
  }
523
- const words = intentWords(text);
904
+ if (!lookVerb(words)) return null;
905
+ clearCardLine(root);
524
906
  const looked = readGoggles(words);
525
907
  if (looked.text.startsWith('Actuality. Drift')) {
526
908
  saveReading(root, { drift: looked.text });
@@ -553,16 +935,19 @@ export function lensPath(root) {
553
935
 
554
936
 
555
937
  export function maintainLens(root) {
556
- const text = actualityLine();
938
+ const drift = actualityLine();
939
+ const text = drift || 'quiet (match)';
557
940
  mkdirSync(dirname(lensPath(root)), { recursive: true });
558
941
  writeFileSync(lensPath(root), `${text}\n`);
559
- return ok(text);
942
+ return ok(drift);
560
943
  }
561
944
 
562
945
 
563
946
  const isMain = Boolean(process.argv[1]) && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
564
947
  if (isMain) {
565
- const result = readGoggles(process.argv.slice(2));
566
- process.stdout.write(`${result.text}\n`);
567
- process.exit(result.ok ? 0 : 1);
948
+ const answered = gogglesAnswer(process.argv.slice(2));
949
+ const pane = process.stdout.isTTY && Array.isArray(answered.cards) && answered.cards.length > 0;
950
+ const text = pane ? answered.cards.map((card) => formatCardPane(card)).join('\n\n') : answered.text;
951
+ process.stdout.write(`${text}\n`);
952
+ process.exit(0);
568
953
  }