0xray 4.0.42 → 4.1.1

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 (138) hide show
  1. package/.gitignore.default +4 -1
  2. package/.grok-plugin/plugin.json +1 -1
  3. package/.opencode/skills/orchestrator/SKILL.md +1 -7
  4. package/.plugin/plugin.json +1 -1
  5. package/.xray/features.json +8 -5
  6. package/.xray/features.schema.json +8 -0
  7. package/AGENTS-consumer.md +9 -7
  8. package/AGENTS.md +19 -10
  9. package/LICENSE +1 -1
  10. package/README.md +15 -9
  11. package/SKILLS.md +3 -1
  12. package/dist/.gitignore.default +4 -1
  13. package/dist/AGENTS.md +19 -10
  14. package/dist/CHANGELOG.md +109 -11
  15. package/dist/LICENSE +1 -1
  16. package/dist/README.md +15 -9
  17. package/dist/SKILLS.md +3 -1
  18. package/dist/cli/commands/opencode-install.js +3 -9
  19. package/dist/cli/commands/plugin-commands.d.ts +4 -0
  20. package/dist/cli/commands/plugin-commands.js +84 -41
  21. package/dist/cli/commands/skill-install.js +10 -5
  22. package/dist/cli/goggles-status.d.ts +2 -5
  23. package/dist/cli/goggles-status.js +5 -53
  24. package/dist/cli/index.js +2 -2
  25. package/dist/core/config-loader.js +4 -4
  26. package/dist/core/features-config.d.ts +1 -1
  27. package/dist/core/features-config.js +3 -3
  28. package/dist/enforcement/enforcer-tools.d.ts +1 -1
  29. package/dist/enforcement/enforcer-tools.js +1 -1
  30. package/dist/integrations/cursor/README.md +1 -1
  31. package/dist/integrations/cursor/hooks/hooks.json +6 -3
  32. package/dist/integrations/cursor/hooks/pre-tool-use.js +104 -37
  33. package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +9 -1
  34. package/dist/integrations/grok/grok-cli.js +5 -7
  35. package/dist/integrations/grok/hooks/grok-hook-utils.js +4 -1
  36. package/dist/integrations/grok/hooks/pre-tool-use.js +6 -76
  37. package/dist/integrations/grok/hooks/session-start.js +4 -2
  38. package/dist/integrations/hermes-agent/bridge.mjs +17 -1
  39. package/dist/integrations/hooks/delegation-gate-runtime.mjs +3 -2
  40. package/dist/integrations/hooks/goggles-mcp.mjs +23 -181
  41. package/dist/integrations/hooks/goggles-pipeline.mjs +606 -571
  42. package/dist/integrations/hooks/goggles-planes.json +95 -46
  43. package/dist/integrations/hooks/plates.cjs +84 -0
  44. package/dist/integrations/hooks/pre-tool-speak.mjs +74 -0
  45. package/dist/integrations/hooks/skill-seek.mjs +115 -0
  46. package/dist/integrations/hooks/station-hook-runtime.cjs +4 -1
  47. package/dist/integrations/openclaw/hooks/pre-tool-gate-runtime.mjs +24 -7
  48. package/dist/integrations/openclaw/plugin/xray-pre-tool/index.js +26 -2
  49. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  50. package/dist/memory-routing/plates.d.ts +1 -1
  51. package/dist/memory-routing/plates.js +14 -0
  52. package/dist/monitoring/{nudge-watchdog.d.ts → nudge-monitor.d.ts} +14 -5
  53. package/dist/monitoring/{nudge-watchdog.js → nudge-monitor.js} +42 -17
  54. package/dist/nucleus/thin-dispatch.d.ts +1 -1
  55. package/dist/nucleus/thin-dispatch.js +1 -1
  56. package/dist/nucleus/work-freshness.mjs +10 -0
  57. package/dist/plugin/xray-codex-injection.js +20 -0
  58. package/dist/processors/implementations/nudge-processor.d.ts +1 -1
  59. package/dist/processors/implementations/nudge-processor.js +4 -4
  60. package/dist/skills/orchestrator/SKILL.md +1 -7
  61. package/docs-site/docs/plates/activity-log.md +1 -1
  62. package/docs-site/docs/plates/asides.md +35 -0
  63. package/docs-site/docs/plates/bifrost.md +48 -0
  64. package/docs-site/docs/plates/boot.md +2 -2
  65. package/docs-site/docs/plates/centrifuge.md +1 -1
  66. package/docs-site/docs/plates/confer.md +35 -0
  67. package/docs-site/docs/plates/consumer-mcp.md +35 -0
  68. package/docs-site/docs/plates/cursor-compact.md +1 -1
  69. package/docs-site/docs/plates/delegation-gate.md +35 -0
  70. package/docs-site/docs/plates/dynamo.md +35 -0
  71. package/docs-site/docs/plates/foundry.md +35 -0
  72. package/docs-site/docs/plates/glossary.md +7 -3
  73. package/docs-site/docs/plates/goggles.md +12 -1
  74. package/docs-site/docs/plates/governance.md +1 -1
  75. package/docs-site/docs/plates/grok-compact.md +1 -1
  76. package/docs-site/docs/plates/grokbot.md +3 -1
  77. package/docs-site/docs/plates/ground.md +35 -0
  78. package/docs-site/docs/plates/hermes.md +35 -0
  79. package/docs-site/docs/plates/host-pack.md +1 -1
  80. package/docs-site/docs/plates/house.md +1 -1
  81. package/docs-site/docs/plates/index.md +16 -7
  82. package/docs-site/docs/plates/inference-cycle.md +35 -0
  83. package/docs-site/docs/plates/inference-files.md +1 -1
  84. package/docs-site/docs/plates/kept-line.md +1 -1
  85. package/docs-site/docs/plates/kits.md +1 -1
  86. package/docs-site/docs/plates/lens-gate.md +8 -6
  87. package/docs-site/docs/plates/lens-page.md +1 -1
  88. package/docs-site/docs/plates/memory-recall.md +1 -1
  89. package/docs-site/docs/plates/notes-page.md +2 -2
  90. package/docs-site/docs/plates/openclaw.md +35 -0
  91. package/docs-site/docs/plates/opencode.md +35 -0
  92. package/docs-site/docs/plates/orchestration.md +1 -1
  93. package/docs-site/docs/plates/package-face.md +1 -1
  94. package/docs-site/docs/plates/payload-heat.md +1 -1
  95. package/docs-site/docs/plates/pickup-stamp.md +1 -1
  96. package/docs-site/docs/plates/pre-tool.md +1 -1
  97. package/docs-site/docs/plates/processor.md +1 -1
  98. package/docs-site/docs/plates/record-map.md +1 -1
  99. package/docs-site/docs/plates/reflection-page.md +1 -1
  100. package/docs-site/docs/plates/repertoire.md +35 -0
  101. package/docs-site/docs/plates/reporting.md +1 -1
  102. package/docs-site/docs/plates/review.md +1 -1
  103. package/docs-site/docs/plates/routing.md +1 -1
  104. package/docs-site/docs/plates/session-capture.md +1 -1
  105. package/docs-site/docs/plates/site-manual.md +1 -1
  106. package/docs-site/docs/plates/stamp-plate.md +1 -1
  107. package/docs-site/docs/plates/station-card.md +1 -1
  108. package/docs-site/docs/plates/station-heat.md +1 -1
  109. package/docs-site/docs/plates/suit-organs.md +1 -1
  110. package/docs-site/docs/plates/suit-settings.md +1 -1
  111. package/docs-site/docs/plates/suit-wear.md +1 -1
  112. package/docs-site/docs/plates/suit.md +3 -3
  113. package/docs-site/docs/plates/synthesis.md +35 -0
  114. package/docs-site/docs/plates/temperament.md +35 -0
  115. package/docs-site/docs/plates/trail-state.md +2 -2
  116. package/docs-site/docs/plates/work-fresh.md +1 -1
  117. package/docs-site/docs/plates/write-home.md +1 -1
  118. package/llms.txt +14 -6
  119. package/package.json +3 -3
  120. package/scripts/foundry/README.md +5 -3
  121. package/scripts/foundry/mint-suit.cjs +120 -2
  122. package/scripts/foundry/plant/skills/mill/SKILL.md +1 -1
  123. package/scripts/foundry/reconcile-version.mjs +10 -3
  124. package/scripts/foundry/release-gate.mjs +111 -15
  125. package/scripts/foundry/release.mjs +9 -1
  126. package/scripts/foundry/validate-release-docs.mjs +2 -2
  127. package/scripts/foundry/version-manager.mjs +12 -4
  128. package/scripts/mjs/verify-pipeline-facets.mjs +1 -1
  129. package/scripts/node/install-bridges.cjs +91 -46
  130. package/scripts/node/pack-tmp-suit-proof.mjs +37 -10
  131. package/src/integrations/openclaw/plugin/xray-pre-tool/index.js +26 -2
  132. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  133. package/src/skills/orchestrator/SKILL.md +1 -7
  134. package/vendor/@0xray/repertoire/README.md +1 -1
  135. package/xray/agents_template.md +2 -2
  136. package/xray/features.json +8 -5
  137. package/xray/features.schema.json +8 -0
  138. package/docs-site/docs/plates/live-mesh.md +0 -41
@@ -1,144 +1,279 @@
1
1
  /**
2
- * Goggles. Kind 0 name-checks the six outer names.
3
- * A named plane returns its view. Field pipes fill what they can see.
4
- * The view keeps every field for the next look.
5
- * A held plane stays quiet. A leave is denied.
6
- * On this host a search is grep, glob, or a shell that runs one.
7
- * A shell that reads a project source file is a search.
8
- * The lens file stays open. One later read may go on, then the next read stops.
9
- * An opened source file that no lens lists is remembered.
10
- * The next search stops until that file is on a lens and its plate is on disk.
11
- * The hook does not author the plate.
2
+ * Goggles. The plane is state. A session starts on dev.
3
+ * The card is in LENS.md: Entry, Exit, and the other rows.
4
+ * A search inside the current card runs. Any other search returns the card and does not run.
5
+ * Opening a file on that card stays quiet.
6
+ * Opening a file on another plane switches, returns that card, and the open proceeds.
7
+ * A file on no plane stays put and returns the current card.
8
+ * A thing on that plane has its own card. One card can name many plates.
9
+ * A missing plate names the stamp-plate skill. The hook does not write the plate.
10
+ * Compact writes the current card back. It does not add a plane.
12
11
  */
13
12
  import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from 'node:fs';
14
13
  import { createRequire } from 'node:module';
14
+ import { seekSkill, skillIdsFromRow, staleSkillRow } from './skill-seek.mjs';
15
15
  import { dirname, join, resolve } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
17
 
18
18
  const FIELD_ORDER = ['plate', 'entry', 'exit', 'files', 'skills', 'setup', 'teardown', 'worn'];
19
19
  const HERE = dirname(fileURLToPath(import.meta.url));
20
+ /** Map names. A look does not answer them. Their files stay on the lens. */
20
21
  const WORN_PLANES = ['dichotomy', 'syncopate', 'synthesis', 'digest', 'triage', 'loop'];
21
- const CARD_PLANES = ['ground', 'routing', 'house', 'boot', 'governance', 'memory-recall', 'orchestration', 'processor', 'reporting', 'stamp-plate', 'record-map', 'write-home', 'activity-log', 'session-capture', 'suit-wear', 'suit-organs', 'station-card', 'notes-page', 'reflection-page', 'site-manual', 'package-face', 'suit-settings', 'trail-state', 'inference-files', 'grok-compact', 'payload-heat', 'station-heat', 'pickup-stamp', 'cursor-compact', 'work-fresh', 'kept-line', 'lens-gate', 'pre-tool', 'lens-page', 'centrifuge'];
22
- const CARD_FLAVORS = ['digest', 'triage'];
23
- const SCOPE_ZOOM = ['ecosystem', 'part', 'one flow', 'one artifact'];
24
-
25
- /** Empty GOGGLES_PLANES_PATH keeps the file beside the organ. */
26
- export function planesFile() {
27
- const override = process.env.GOGGLES_PLANES_PATH;
28
- if (typeof override === 'string' && override.trim()) return override.trim();
29
- return join(HERE, 'goggles-planes.json');
22
+ export const CARD_PLANES = ['dev', 'integration', 'domain', 'ecosystem'];
23
+
24
+ const PLANE_PATHS = {
25
+ dev: ['src', 'grok-bot', 'docs-site', 'AGENTS.md', 'package.json', 'README.md'],
26
+ integration: [
27
+ 'src/integrations',
28
+ 'src/nucleus',
29
+ 'src/governance',
30
+ 'src/mcps',
31
+ 'src/processors',
32
+ 'src/reporting',
33
+ 'src/core/boot-orchestrator.ts',
34
+ 'grok-bot/lib',
35
+ 'grok-bot/templates',
36
+ ],
37
+ domain: ['docs-site/docs/plates', 'src/integrations/hooks/plates.cjs', '.agents/skills'],
38
+ ecosystem: ['scripts/foundry', 'node_modules/@0xray'],
39
+ };
40
+
41
+ const PLANE_ROWS = {
42
+ dev: {
43
+ from: '',
44
+ digest: 'This checkout.',
45
+ plate: '',
46
+ entry: 'package.json, src, and grok-bot/OP-PROC.md.',
47
+ exit: 'A change that stays in this repo.',
48
+ files: 'src, grok-bot, docs-site, AGENTS.md, package.json, README.md',
49
+ skills: 'orchestrator, researcher, project-analysis, git-workflow, tech-writer',
50
+ setup: 'You are in this repo.',
51
+ teardown: 'Leave other repos alone.',
52
+ worn: '',
53
+ },
54
+ integration: {
55
+ from: 'dev',
56
+ digest: 'How one part calls the next.',
57
+ plate: '',
58
+ entry: 'The hook, the dispatch, or the bridge this file sits on.',
59
+ exit: 'The next call. Stop there.',
60
+ files: 'src/integrations, src/nucleus, src/governance, src/mcps, src/processors, src/reporting, src/core/boot-orchestrator.ts, grok-bot/lib, grok-bot/templates',
61
+ skills: 'routing, orchestration, boot-orchestrator, enforcer, hermes-agent',
62
+ setup: 'One connection at a time.',
63
+ teardown: 'Do not sweep the rest of the repo.',
64
+ worn: '',
65
+ },
66
+ domain: {
67
+ from: 'dev',
68
+ digest: 'The drawing of one area.',
69
+ plate: 'docs-site/docs/plates',
70
+ entry: 'The plate under docs-site/docs/plates, or the plate reader.',
71
+ exit: 'The entry and exit written on that plate.',
72
+ files: 'docs-site/docs/plates, src/integrations/hooks/plates.cjs, .agents/skills',
73
+ skills: 'lens-gate, stamp-plate, record-map, station-card, memory-recall',
74
+ setup: 'One plate is in the frame.',
75
+ teardown: 'The plate is the drawing. The code stays on dev or integration.',
76
+ worn: '',
77
+ },
78
+ ecosystem: {
79
+ from: 'dev',
80
+ digest: 'What this repo wears from outside.',
81
+ plate: '',
82
+ entry: 'scripts/foundry, and the @0xray packages this repo wears.',
83
+ exit: 'Back to this checkout when the change is here.',
84
+ files: 'scripts/foundry, node_modules/@0xray',
85
+ skills: 'foundry, suit-wear, consumer-mcp, house, package-face',
86
+ setup: 'The outside package is not this repo.',
87
+ teardown: 'Do not edit a worn package from here.',
88
+ worn: 'node_modules/@0xray',
89
+ },
90
+ };
91
+
92
+ function builtinCard(id) {
93
+ const row = PLANE_ROWS[id];
94
+ if (!row) return '';
95
+ const lines = [
96
+ ['Plane', id],
97
+ ['From', row.from],
98
+ ['Digest', row.digest],
99
+ ['Plate', row.plate],
100
+ ['Entry', row.entry],
101
+ ['Exit', row.exit],
102
+ ['Files', row.files],
103
+ ['Skills', row.skills],
104
+ ['Setup', row.setup],
105
+ ['Teardown', row.teardown],
106
+ ['Worn', row.worn],
107
+ ];
108
+ return lines.map(([key, value]) => `${key}: ${value}`.trimEnd()).join('\n');
30
109
  }
31
110
 
32
- function drawnPlanes() {
33
- try {
34
- const data = JSON.parse(readFileSync(planesFile(), 'utf8'));
35
- return Array.isArray(data.planes) ? data.planes.map((name) => String(name)) : [];
36
- } catch {
37
- return [];
111
+ function planeOfPath(file) {
112
+ const rel = String(file || '').replace(/\\/g, '/').replace(/^\.\//, '');
113
+ let best = '';
114
+ let winner = '';
115
+ for (const [id, paths] of Object.entries(PLANE_PATHS)) {
116
+ for (const prefix of paths) {
117
+ if (rel === prefix || rel.startsWith(`${prefix}/`)) {
118
+ if (prefix.length > best.length) {
119
+ best = prefix;
120
+ winner = id;
121
+ }
122
+ }
123
+ }
38
124
  }
125
+ return winner;
126
+ }
127
+
128
+ /** Empty GOGGLES_PLANES_PATH keeps the file beside the organ. */
129
+ const STAMP_LINE = 'Stamp: .agents/skills/stamp-plate/SKILL.md — one plate for this card.';
130
+
131
+ function mapEntry(id) {
132
+ const table = overlayTable();
133
+ const entry = table[id];
134
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return null;
135
+ return entry;
39
136
  }
40
137
 
41
- export function actualityOf(map, worn) {
42
- const left = Array.isArray(map) ? map : [];
43
- const right = Array.isArray(worn) ? worn : [];
44
- const same = left.length === right.length && left.every((name, index) => name === right[index]);
45
- return same ? '' : 'Actuality. Drift: worn is not the map.';
138
+ /** A plane, or a thing that has a card. */
139
+ export function isCardName(id) {
140
+ const name = String(id || '').trim();
141
+ if (!name || name.includes(' ')) return false;
142
+ if (CARD_PLANES.includes(name)) return true;
143
+ return Boolean(mapEntry(name));
46
144
  }
47
145
 
48
- export function actualityLine() {
49
- return actualityOf(drawnPlanes(), WORN_PLANES);
146
+ function thingFiles(entry) {
147
+ if (!entry) return [];
148
+ if (Array.isArray(entry.files)) return entry.files.map(String);
149
+ if (typeof entry.file === 'string' && entry.file.trim()) return [entry.file.trim()];
150
+ return [];
50
151
  }
51
152
 
52
- function acceptedPlanes() {
53
- const map = new Set(drawnPlanes());
54
- return WORN_PLANES.filter((name) => map.has(name));
153
+ function planeSpan(file) {
154
+ const rel = String(file || '').replace(/\\/g, '/').replace(/^\.\//, '');
155
+ let best = 0;
156
+ for (const paths of Object.values(PLANE_PATHS)) {
157
+ for (const prefix of paths) {
158
+ if (rel === prefix || rel.startsWith(`${prefix}/`)) best = Math.max(best, prefix.length);
159
+ }
160
+ }
161
+ return best;
55
162
  }
56
163
 
57
- function readingLine(name, scope) {
58
- return scope ? `The reading is ${name}. Scope is ${scope}.` : `The reading is ${name}.`;
164
+ export const PATH_OWNER = {
165
+ 'src/integrations/hooks/station-hook-runtime.cjs': 'station-card',
166
+ 'src/integrations/hooks/wake-cascade.cjs': 'record-map',
167
+ 'src/integrations/hooks/session-note-index.cjs': 'notes-page',
168
+ 'src/reporting/framework-reporting-system.ts': 'reporting',
169
+ 'src/inference/inference-accumulator.ts': 'inference-files',
170
+ 'src/integrations/hooks/goggles-pipeline.mjs': 'lens-gate',
171
+ };
172
+
173
+ function covers(listed, rel) {
174
+ return rel === listed || rel.startsWith(`${listed}/`);
59
175
  }
60
176
 
61
- function organDeny(reason) {
62
- return { gate: 'goggles', decision: 'deny', reason };
177
+ function rootsOf(entry) {
178
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return [];
179
+ if (Array.isArray(entry.root)) return entry.root.map(String).filter((item) => item.trim());
180
+ if (typeof entry.root === 'string' && entry.root.trim()) return [entry.root.trim()];
181
+ return [];
63
182
  }
64
183
 
65
- function plateIdsIn(spoken) {
66
- const ids = [];
67
- const re = /docs-site\/docs\/plates\/([a-z0-9-]+)\.md/ig;
68
- let match;
69
- while ((match = re.exec(String(spoken || '')))) ids.push(match[1]);
70
- return ids;
184
+ function cardMatchPaths(id) {
185
+ if (CARD_PLANES.includes(id)) return PLANE_PATHS[id] || [];
186
+ const row = mapEntry(id);
187
+ if (!row) return [];
188
+ return [...thingFiles(row), ...rootsOf(row)];
71
189
  }
72
190
 
73
- function associatedPlates(plane) {
74
- try {
75
- const data = JSON.parse(readFileSync(planesFile(), 'utf8'));
76
- const entry = data[plane];
77
- const listed = entry && Array.isArray(entry.plates) ? entry.plates.map(String) : [];
78
- return new Set([plane, ...listed]);
79
- } catch {
80
- return new Set([plane]);
191
+ /** Longest listed path. A root may equal the plane. A tie uses PATH_OWNER, or stays the plane. */
192
+ export function thingOfPath(file) {
193
+ const rel = String(file || '').replace(/\\/g, '/').replace(/^\.\//, '');
194
+ const table = overlayTable();
195
+ let best = -1;
196
+ const hits = [];
197
+ for (const [id, entry] of Object.entries(table)) {
198
+ if (CARD_PLANES.includes(id)) continue;
199
+ const row = entry && typeof entry === 'object' && !Array.isArray(entry) ? entry : null;
200
+ if (!row) continue;
201
+ const span = planeSpan(rel);
202
+ const consider = (listed, floor) => {
203
+ if (!covers(listed, rel) || listed.length < floor) return;
204
+ if (listed.length > best) {
205
+ best = listed.length;
206
+ hits.length = 0;
207
+ }
208
+ if (listed.length === best) hits.push({ id, listed });
209
+ };
210
+ for (const listed of thingFiles(row)) consider(listed, span + 1);
211
+ for (const listed of rootsOf(row)) consider(listed, span);
81
212
  }
213
+ const ids = [];
214
+ for (const hit of hits) {
215
+ if (!ids.includes(hit.id)) ids.push(hit.id);
216
+ }
217
+ if (ids.length === 1) return ids[0];
218
+ if (!ids.length) return '';
219
+ const owner = PATH_OWNER[hits[0].listed];
220
+ return owner && ids.includes(owner) ? owner : '';
82
221
  }
83
222
 
84
- function planeWord(name, text) {
85
- return new RegExp(`(?:^|[^A-Za-z0-9-])${String(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:[^A-Za-z0-9-]|$)`, 'i').test(String(text || ''));
223
+
224
+ function platePath(id) {
225
+ if (CARD_PLANES.includes(id)) return '';
226
+ const dir = findPlatesDir(HERE);
227
+ if (!dir) return '';
228
+ const abs = join(dir, `${id}.md`);
229
+ return existsSync(abs) ? join('docs-site', 'docs', 'plates', `${id}.md`) : '';
86
230
  }
87
231
 
88
- function withoutPaths(text) {
89
- return String(text || '').replace(/(?:[\w.@~-]+\/)+[\w.-]+/g, ' ');
232
+ function showCard(id, block) {
233
+ if (CARD_PLANES.includes(id)) return block;
234
+ const path = platePath(id);
235
+ let shown = block;
236
+ if (path && /^Plate:[ \t]*$/m.test(shown)) shown = shown.replace(/^Plate:[ \t]*$/m, `Plate: ${path}`);
237
+ if (!path && !shown.includes('Stamp:')) shown = `${shown}\n${STAMP_LINE}`;
238
+ return shown;
90
239
  }
91
240
 
92
- export function organStop(held, action) {
93
- if (!held) return null;
94
- if (held.drift) return organDeny(held.drift);
95
- const plane = held.plane;
96
- if (!plane) return null;
97
- const text = String(action?.text || '');
98
- const paths = (action?.paths || []).map(String).filter(Boolean);
99
- const tool = String(action?.tool || '');
100
- const spoken = [text, ...paths].join('\n');
101
- const drawn = plateIdsIn(spoken);
102
- if (drawn.length) {
103
- const allowed = associatedPlates(plane);
104
- const foreign = drawn.filter((id) => !allowed.has(id));
105
- if (foreign.length) {
106
- return organDeny(`The reading is ${plane}. That drawing is not the plane.`);
107
- }
108
- }
109
- const otherFile = otherPlaneFile(plane, spoken);
110
- if (otherFile) return organDeny(`The reading is ${plane}. The action is ${otherFile}.`);
111
- const stripped = withoutPaths(text);
112
- const others = acceptedPlanes().filter((name) => name !== plane && planeWord(name, stripped));
113
- const namesThis = planeWord(plane, stripped);
114
- if (others.length > 1 || (others.length === 1 && namesThis)) {
115
- return organDeny(`The reading is ${plane}. The action names two planes.`);
116
- }
117
- if (others.length === 1) {
118
- return organDeny(`The reading is ${plane}. The action is ${others[0]}.`);
241
+ function readHold(root) {
242
+ try {
243
+ const data = JSON.parse(readFileSync(join(root, '.xray', 'state', 'goggles-plane.json'), 'utf8'));
244
+ const named = typeof data.card === 'string' ? data.card : '';
245
+ const plane = CARD_PLANES.includes(data.plane) ? data.plane : '';
246
+ const card = isCardName(named) ? named : (plane || '');
247
+ if (plane) return { plane, card: card || plane };
248
+ if (card && CARD_PLANES.includes(card)) return { plane: card, card };
249
+ } catch {
250
+ /* no hold yet */
119
251
  }
120
- return scopeStop(plane, held.scope, tool, paths);
252
+ return null;
121
253
  }
122
254
 
123
- function scopeStop(plane, scope, tool, paths) {
124
- if (!scope || scope === 'ecosystem') return null;
125
- if (scope === 'part') {
126
- const dirs = new Set(paths.map((file) => dirname(file)));
127
- if (dirs.size > 1) return organDeny(`The reading is ${plane}. Scope is part. This action crosses parts.`);
128
- return null;
255
+ function planeForCard(id) {
256
+ if (CARD_PLANES.includes(id)) return id;
257
+ for (const file of thingFiles(mapEntry(id))) {
258
+ const plane = planeOfPath(file);
259
+ if (plane) return plane;
129
260
  }
130
- if (scope === 'one flow') {
131
- if (/grep|search|glob|read|open/i.test(tool) && paths.length !== 1) {
132
- return organDeny(`The reading is ${plane}. Scope is one flow. This action is not one flow.`);
133
- }
134
- return null;
135
- }
136
- if (scope === 'one artifact') {
137
- if (/bash|shell/i.test(tool) || paths.length !== 1) {
138
- return organDeny(`The reading is ${plane}. Scope is one artifact. This action is wider.`);
139
- }
261
+ return 'dev';
262
+ }
263
+
264
+ export function planesFile() {
265
+ const override = process.env.GOGGLES_PLANES_PATH;
266
+ if (typeof override === 'string' && override.trim()) return override.trim();
267
+ return join(HERE, 'goggles-planes.json');
268
+ }
269
+
270
+ function drawnPlanes() {
271
+ try {
272
+ const data = JSON.parse(readFileSync(planesFile(), 'utf8'));
273
+ return Array.isArray(data.planes) ? data.planes.map((name) => String(name)) : [];
274
+ } catch {
275
+ return [];
140
276
  }
141
- return null;
142
277
  }
143
278
 
144
279
  function readingPath(root) {
@@ -157,61 +292,89 @@ function loadReading(root) {
157
292
  }
158
293
  }
159
294
 
160
- function saveReading(root, rec) {
161
- const file = readingPath(root);
162
- mkdirSync(dirname(file), { recursive: true });
163
- writeFileSync(file, `${JSON.stringify(rec)}\n`);
164
- }
165
-
166
295
  function clearReading(root) {
167
296
  const file = readingPath(root);
168
297
  if (existsSync(file)) unlinkSync(file);
169
298
  }
170
299
 
171
- function findScopes(words) {
172
- let rest = ` ${words.join(' ').toLowerCase()} `;
173
- const hits = [];
174
- const ordered = [...SCOPE_ZOOM].sort((a, b) => b.length - a.length);
175
- for (const scope of ordered) {
176
- const needle = ` ${scope} `;
177
- if (!rest.includes(needle)) continue;
178
- hits.push(scope);
179
- rest = rest.split(needle).join(' ');
300
+ function lensRoot(projectRoot) {
301
+ if (typeof projectRoot === 'string' && projectRoot.trim() && projectRoot.trim() !== '.') {
302
+ return resolve(projectRoot.trim());
180
303
  }
181
- return hits;
304
+ return stateRoot();
182
305
  }
183
306
 
184
- function kindPastOne(words) {
185
- const lower = words.map((word) => word.toLowerCase());
186
- if (lower.length === 1 && /^\d+$/.test(lower[0]) && Number(lower[0]) > 1) return true;
187
- for (let i = 0; i < lower.length; i += 1) {
188
- if (!/^(kind|pull|pull-up|level)$/.test(lower[i])) continue;
189
- const next = lower[i + 1];
190
- if (next && /^\d+$/.test(next) && Number(next) > 1) return true;
191
- }
192
- return false;
307
+ function cardRow(block, label) {
308
+ const match = String(block || '').match(new RegExp(`^${label}:[ \t]*(.*)$`, 'm'));
309
+ return match ? match[1].trim() : '';
310
+ }
311
+
312
+ function splitRow(block, label) {
313
+ return cardRow(block, label).split(',').map((part) => part.trim()).filter(Boolean);
314
+ }
315
+
316
+ function installedPack(root) {
317
+ return /\/node_modules\/0xray$/.test(String(root || '').replace(/\\/g, '/'));
193
318
  }
194
319
 
195
- function isKindZero(lower) {
196
- if (lower.includes('actuality')) return true;
197
- if (lower.length === 1 && lower[0] === '0') return true;
198
- for (let i = 0; i < lower.length; i += 1) {
199
- if ((lower[i] === 'kind' || lower[i] === 'level') && lower[i + 1] === '0') return true;
320
+ export function liveDrift(block, root) {
321
+ if (installedPack(root)) return '';
322
+ const files = splitRow(block, 'Files');
323
+ const worn = cardRow(block, 'Worn');
324
+ const held = Boolean(worn) && existsSync(join(root, worn));
325
+ const packed = (rel) => held && (rel.startsWith('src/') || rel.startsWith('grok-bot/'));
326
+ for (const rel of files) {
327
+ if (existsSync(join(root, rel))) continue;
328
+ if (packed(rel)) continue;
329
+ return `Drift: ${rel} is not there`;
330
+ }
331
+ const skills = cardRow(block, 'Skills');
332
+ if (skills.includes('/') && !existsSync(join(root, skills)) && !packed(skills)) {
333
+ return `Drift: ${skills} is not there`;
200
334
  }
201
- return false;
335
+ return '';
202
336
  }
203
337
 
204
- function namesTheSet(lower) {
205
- const joined = lower.join(' ');
206
- if (joined === 'kind 1' || joined === 'level 1') return true;
207
- const mentionsSet = lower.includes('outer') || lower.includes('outer-loop');
208
- if (!mentionsSet) return false;
209
- const planes = acceptedPlanes().filter((name) => lower.includes(name));
210
- if (planes.length === 0) return true;
211
- return planes.length === 1 && planes[0] === 'loop' && findScopes(lower).length === 0;
338
+ function viewFromBlock(block, drift) {
339
+ const plane = cardRow(block, 'Plane');
340
+ return {
341
+ card: cardRow(block, 'Card') || plane,
342
+ plane,
343
+ stamp: cardRow(block, 'Stamp'),
344
+ scope: '',
345
+ from: cardRow(block, 'From'),
346
+ digest: cardRow(block, 'Digest'),
347
+ plate: cardRow(block, 'Plate'),
348
+ plates: splitRow(block, 'Plates'),
349
+ entry: cardRow(block, 'Entry'),
350
+ exit: cardRow(block, 'Exit'),
351
+ files: splitRow(block, 'Files'),
352
+ skills: cardRow(block, 'Skills'),
353
+ setup: cardRow(block, 'Setup'),
354
+ teardown: cardRow(block, 'Teardown'),
355
+ worn: cardRow(block, 'Worn'),
356
+ exam: drift || '',
357
+ };
358
+ }
359
+
360
+ function storedCard(root, id) {
361
+ const kept = readLensCard(root, id);
362
+ if (kept) return kept;
363
+ return fillLensCard(root, id);
212
364
  }
213
365
 
214
- function gogglesAnswer(argv, projectRoot) {
366
+ function presentCard(projectRoot, id) {
367
+ const block = storedCard(lensRoot(projectRoot), id);
368
+ if (!block) return null;
369
+ const shown = showCard(id, block);
370
+ const drift = liveDrift(shown, sourceRoot(findPlatesDir(HERE)));
371
+ return {
372
+ text: drift ? `${shown}\n${drift}` : shown,
373
+ view: viewFromBlock(shown, drift),
374
+ };
375
+ }
376
+
377
+ function gogglesAnswer(argv, projectRoot, hold) {
215
378
  const words = [];
216
379
  for (const raw of argv || []) {
217
380
  const arg = String(raw || '').trim();
@@ -219,120 +382,49 @@ function gogglesAnswer(argv, projectRoot) {
219
382
  words.push(arg);
220
383
  }
221
384
  const lower = words.map((word) => word.toLowerCase());
222
- const none = (text) => ({ text, cards: null });
223
- if (lower.includes('calling')) return none('');
224
- if (kindPastOne(words)) return none('');
225
- if (words.length === 0) return none('');
226
- if (namesTheSet(lower)) return none('Name one plane.');
227
- if (isKindZero(lower)) {
228
- if (findScopes(words).length) return none('');
229
- if (acceptedPlanes().some((name) => lower.includes(name))) return none('');
230
- return none(actualityLine());
231
- }
232
- const flavors = CARD_FLAVORS.filter((name) => lower.includes(name));
233
- if (flavors.length > 1) return none('');
234
- if (flavors.length === 1) {
235
- const others = acceptedPlanes().filter((name) => name !== flavors[0] && lower.includes(name));
236
- if (others.length) return none('');
237
- const cards = collectCards(flavors[0], lower, projectRoot);
238
- if (!cards) return none('');
239
- return { text: cards.map((card) => formatCardText(card)).join('\n\n'), cards };
240
- }
241
- const planes = acceptedPlanes().filter((name) => lower.includes(name));
242
- if (planes.length !== 1) {
243
- if (!planes.length && cardPlanesNamed(lower).length) {
244
- const cards = collectCards('digest', lower, projectRoot);
245
- if (cards && cards.length) {
246
- return { text: cards.map((card) => formatCardText(card)).join('\n\n'), cards };
247
- }
248
- }
249
- return none('');
250
- }
251
- const scopes = findScopes(words.filter((word) => word.toLowerCase() !== planes[0]));
252
- if (scopes.length > 1) return none('');
253
- const line = readingLine(planes[0], scopes[0] || '');
254
- const file = moduleFile(planes[0]);
255
- return none(file ? `${line} File: ${file}` : line);
385
+ const named = cardsNamed(lower);
386
+ const root = lensRoot(projectRoot);
387
+ let id = '';
388
+ if (named.length === 1) id = named[0];
389
+ else if (words.length === 0) {
390
+ if (hold) ensurePlane(root);
391
+ id = currentCard(root);
392
+ } else return { text: 'Name one plane.', cards: null };
393
+ if (hold && named.length === 1) holdPlane(root, id);
394
+ const card = presentCard(root, id);
395
+ if (!card) return { text: 'Name one plane.', cards: null };
396
+ return { text: card.text, cards: [card.view] };
256
397
  }
257
398
 
258
399
  export function readGoggles(argv, projectRoot) {
259
- return ok(gogglesAnswer(argv, projectRoot).text);
400
+ return ok(gogglesAnswer(argv, projectRoot, true).text);
260
401
  }
261
402
 
262
403
  export function lookCards(argv, projectRoot) {
263
- return gogglesAnswer(argv, projectRoot).cards;
404
+ return gogglesAnswer(argv, projectRoot, false).cards;
264
405
  }
265
406
 
266
- function cardPlanesNamed(lower) {
267
- return CARD_PLANES.filter((name) => lower.includes(name));
407
+ function cardsNamed(lower) {
408
+ const found = [];
409
+ for (const word of lower) {
410
+ if (isCardName(word) && !found.includes(word)) found.push(word);
411
+ }
412
+ return found;
268
413
  }
269
414
 
270
415
  function listedFiles(plane) {
271
416
  return Array.isArray(plane.files) ? plane.files.map(String) : [];
272
417
  }
273
418
 
274
- function cascadeApi() {
275
- return createRequire(import.meta.url)('./wake-cascade.cjs');
276
- }
277
-
278
- function sleeveApi() {
279
- return createRequire(import.meta.url)('./sleeve.cjs');
280
- }
281
-
282
- function appendLining(text, view) {
283
- let next = text;
284
- if (view.cascade) next = `${next}\n${cascadeApi().formatCascadeReading(view.cascade)}`;
285
- if (view.sleeve) next = `${next}\n${sleeveApi().formatSleeveReading(view.sleeve)}`;
286
- return next;
287
- }
288
-
289
- function cardView(plane, scope, flavor, root, cascadeRoot) {
290
- const listed = listedFiles(plane);
291
- const view = {
292
- plane: plane.id,
293
- flavor,
294
- scope: scope || '',
295
- from: plane.id === 'ground' ? '' : (plane.from || 'ground'),
296
- digest: plane.digest || '',
297
- plate: plane.plate || '',
298
- plates: Array.isArray(plane.plates) ? plane.plates : [],
299
- entry: plane.entry || '',
300
- exit: plane.exit || '',
301
- files: listed,
302
- skills: plane.skills || '',
303
- setup: plane.setup || '',
304
- teardown: plane.teardown || '',
305
- worn: plane.worn || '',
306
- };
307
- view.exam = examinePlane(plane, root).text;
308
- if (cascadeApi().isCascadePlane(plane.id)) {
309
- try {
310
- view.cascade = cascadeApi().readWakeCascade(cascadeRoot || stateRoot());
311
- } catch {
312
- view.cascade = null;
313
- }
314
- }
315
- if (sleeveApi().isSleevePlane(plane.id)) {
316
- try {
317
- view.sleeve = sleeveApi().readSleeve(cascadeRoot || stateRoot());
318
- } catch {
319
- view.sleeve = null;
320
- }
321
- }
322
- if (flavor === 'triage') {
323
- const filled = new Set(filledOf(plane));
324
- view.empty = FIELD_ORDER.filter((name) => !filled.has(name));
325
- }
326
- return view;
327
- }
328
-
329
419
  function cardRows(view) {
330
- const rows = [
420
+ const rows = [];
421
+ if (view.card && view.card !== view.plane) rows.push(['Card', view.card]);
422
+ rows.push(
331
423
  ['Plane', view.plane],
332
424
  ['From', view.from],
333
425
  ['Digest', view.digest],
334
426
  ['Plate', view.plate],
335
- ];
427
+ );
336
428
  if (Array.isArray(view.plates) && view.plates.length) rows.push(['Plates', view.plates.join(', ')]);
337
429
  rows.push(
338
430
  ['Entry', view.entry],
@@ -346,52 +438,19 @@ function cardRows(view) {
346
438
  return rows;
347
439
  }
348
440
 
349
- function formatCardText(view) {
350
- const rows = cardRows(view);
351
- let text = rows.map(([key, value]) => `${key}: ${value}`.trimEnd()).join('\n');
352
- if (view.flavor === 'triage') {
353
- const lines = [];
354
- if (view.empty.length) lines.push(`Empty: ${view.empty.join(', ')}`);
355
- lines.push(view.exam);
356
- text = `${text}\n${lines.join('\n')}`;
357
- } else if (typeof view.exam === 'string' && view.exam.startsWith('Drift:')) {
358
- text = `${text}\n${view.exam}`;
359
- }
360
- return appendLining(text, view);
361
- }
362
-
363
441
  export function formatCardPane(view) {
364
442
  const rows = cardRows(view);
365
- if (view.flavor === 'triage') {
366
- if (Array.isArray(view.empty) && view.empty.length) rows.push(['Empty', view.empty.join(', ')]);
367
- if (view.exam) rows.push(['', view.exam]);
368
- } else if (typeof view.exam === 'string' && view.exam.startsWith('Drift:')) {
443
+ if (typeof view.exam === 'string' && view.exam.startsWith('Drift:')) {
369
444
  rows.push(['', view.exam]);
370
445
  }
371
446
  const body = rows.map(([label, value]) => (label ? `${label.padEnd(8)} ${value}` : value).trimEnd());
372
- const title = `${view.flavor} · ${view.plane}`;
447
+ const title = view.card && view.card !== view.plane ? view.card : view.plane;
373
448
  const width = Math.max(title.length + 2, ...body.map((line) => line.length));
374
449
  const top = `┌ ${title} ${'─'.repeat(Math.max(0, width - title.length - 1))}┐`;
375
450
  const mid = body.map((line) => `│ ${line.padEnd(width)} │`);
376
451
  const bot = `└${'─'.repeat(width + 2)}┘`;
377
452
  const pane = [top, ...mid, bot].join('\n');
378
- return appendLining(pane, view);
379
- }
380
-
381
- function collectCards(flavor, lower, projectRoot) {
382
- const named = cardPlanesNamed(lower);
383
- const scopes = findScopes(lower.filter((word) => word !== flavor && !named.includes(word)));
384
- if (scopes.length > 1) return null;
385
- const scope = scopes[0] || '';
386
- const platesDir = findPlatesDir(HERE);
387
- const ids = named.length ? named : CARD_PLANES;
388
- const root = sourceRoot(platesDir);
389
- const cards = [];
390
- for (const id of ids) {
391
- const plane = growPlane(id, platesDir, stateRoot());
392
- cards.push(cardView(plane, scope, flavor, root, projectRoot));
393
- }
394
- return cards;
453
+ return pane;
395
454
  }
396
455
 
397
456
  export function findPlatesDir(start) {
@@ -530,7 +589,7 @@ export function filledOf(plane) {
530
589
  }
531
590
 
532
591
  function plateSource(id, platesDir) {
533
- if (!platesDir || id === 'ground') return { body: '', path: '' };
592
+ if (!platesDir) return { body: '', path: '' };
534
593
  const abs = join(platesDir, `${id}.md`);
535
594
  if (!existsSync(abs)) return { body: '', path: '' };
536
595
  return {
@@ -567,7 +626,11 @@ export function assemblePlane(id, platesDir) {
567
626
  const doors = src.body ? doorsOf(src.body) : { entry: null, exit: null };
568
627
  const flow = src.body ? flowEnds(src.body) : null;
569
628
  const digest = extra.digest || (src.body ? takeOf(stripFrontmatter(src.body)) : '');
570
- const files = Array.isArray(extra.files) ? extra.files.map(String) : [];
629
+ const listed = Array.isArray(extra.files)
630
+ ? extra.files.map(String)
631
+ : (typeof extra.file === 'string' && extra.file.trim() ? [extra.file.trim()] : []);
632
+ const roots = rootsOf(extra);
633
+ const files = [...roots.filter((item) => !listed.includes(item)), ...listed];
571
634
  return {
572
635
  id,
573
636
  digest,
@@ -602,25 +665,6 @@ function sourceRoot(platesDir) {
602
665
  return platesDir ? repoRootFrom(platesDir) : stateRoot();
603
666
  }
604
667
 
605
- function viewsPath(root) {
606
- return join(root, '.xray', 'state', 'goggles-views.json');
607
- }
608
-
609
- function loadViews(root) {
610
- try {
611
- const data = JSON.parse(readFileSync(viewsPath(root), 'utf8'));
612
- return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
613
- } catch {
614
- return {};
615
- }
616
- }
617
-
618
- function saveViews(root, views) {
619
- const file = viewsPath(root);
620
- mkdirSync(dirname(file), { recursive: true });
621
- writeFileSync(file, `${JSON.stringify(views, null, 2)}\n`);
622
- }
623
-
624
668
  function cluesOf(body) {
625
669
  if (!body) return [];
626
670
  const clues = [];
@@ -714,29 +758,8 @@ function skillFor(id, root) {
714
758
  return existsSync(join(root, rel)) ? rel : '';
715
759
  }
716
760
 
717
- function mappedFile(name) {
718
- try {
719
- const data = JSON.parse(readFileSync(planesFile(), 'utf8'));
720
- const entry = data[name];
721
- if (!entry || typeof entry !== 'object') return '';
722
- if (typeof entry.file === 'string' && entry.file) return entry.file;
723
- } catch {
724
- return '';
725
- }
726
- return '';
727
- }
728
-
729
- function moduleFile(name) {
730
- const mapped = mappedFile(name);
731
- if (mapped) return mapped;
732
- const platesDir = findPlatesDir(HERE);
733
- const root = sourceRoot(platesDir);
734
- const hit = sourceTexts(root).find((file) => file.rel.endsWith(`/${name}.ts`) || file.rel.endsWith(`/${name}.mjs`));
735
- return hit ? hit.rel : '';
736
- }
737
-
738
761
  function hintLine(name) {
739
- const cards = lookCards(['digest', name]);
762
+ const cards = lookCards([name]);
740
763
  const files = cards && cards[0] && Array.isArray(cards[0].files) ? cards[0].files : [];
741
764
  return files.length ? `${name}: ${files.join(', ')}` : '';
742
765
  }
@@ -749,7 +772,6 @@ export function suitHint(text) {
749
772
  }
750
773
 
751
774
  const DEEP_SEARCH = /search_codebase|find_implementation|get_documentation/i;
752
- const HOLDS_ON_NAME = new Set(['dichotomy', 'syncopate', 'synthesis']);
753
775
  const SHELL_SEARCH = /(?:^|&&|\|\||[;&|`(\n])\s*(?:(?:\/[\w.+/-]+\/)|(?:npx\s+))?(?:rg|grep|ag|ack|fd|find)\b|\bgit\s+grep\b/;
754
776
 
755
777
  function shellSearches(text) {
@@ -769,15 +791,6 @@ function mappedPlaneFiles(name) {
769
791
  }
770
792
  }
771
793
 
772
- function otherPlaneFile(held, spoken) {
773
- const text = String(spoken || '');
774
- for (const name of acceptedPlanes()) {
775
- if (name === held) continue;
776
- if (mappedPlaneFiles(name).some((file) => text.includes(file))) return name;
777
- }
778
- return '';
779
- }
780
-
781
794
  function mapFiles() {
782
795
  try {
783
796
  const data = JSON.parse(readFileSync(planesFile(), 'utf8'));
@@ -826,186 +839,205 @@ function shellReadsSource(text) {
826
839
  return SOURCE_READER.test(spoken) && openedSourcePaths(spoken).length > 0;
827
840
  }
828
841
 
829
- function isLensWrite(toolName) {
830
- return /write|edit|replace/i.test(String(toolName || ''));
831
- }
832
-
833
- function debtFile(root) {
834
- return join(root, '.xray', 'state', 'goggles-lens-debt.json');
842
+ /** One plane: the files the suit continues with. A look with no plane stops. Two planes stay quiet. */
843
+ function researchCall(toolName, text) {
844
+ const tool = String(toolName || '');
845
+ const spoken = String(text || '');
846
+ if (/researcher|explorer|deep[- ]?research/i.test(tool) || DEEP_SEARCH.test(tool)) return true;
847
+ if (/bash|shell/i.test(tool) && (/researcher|explorer|deep[- ]?research/i.test(spoken) || DEEP_SEARCH.test(spoken) || shellSearches(spoken) || shellReadsSource(spoken))) return true;
848
+ if (/^(grep|glob)$/i.test(tool)) return true;
849
+ return /^read_file$/i.test(tool) && !opensLensFile(spoken);
835
850
  }
836
851
 
837
- function loadDebt(root) {
838
- try {
839
- const data = JSON.parse(readFileSync(debtFile(root), 'utf8'));
840
- return Array.isArray(data.paths) ? data.paths.filter((file) => typeof file === 'string') : [];
841
- } catch {
842
- return [];
843
- }
852
+ export function lensPlane(text) {
853
+ const hint = suitHint(String(text || ''));
854
+ if (hint === 'Name one plane.') return { stop: true, files: [] };
855
+ if (!hint || hint.includes('\n')) return { stop: false, files: [] };
856
+ const mark = hint.indexOf(': ');
857
+ if (mark < 0) return { stop: false, files: [] };
858
+ const files = hint.slice(mark + 2).split(',').map((part) => part.trim()).filter(Boolean);
859
+ return { stop: false, files };
844
860
  }
845
861
 
846
- function saveDebt(root, paths) {
847
- const unique = [];
848
- for (const file of paths) {
849
- if (!unique.includes(file)) unique.push(file);
850
- }
851
- const dest = debtFile(root);
852
- if (!unique.length) {
853
- try {
854
- unlinkSync(dest);
855
- } catch {
856
- /* already clear */
857
- }
858
- return;
859
- }
862
+ export function holdPlane(root, id) {
863
+ const name = String(id || '').trim();
864
+ if (!name || name.includes(' ') || !isCardName(name)) return;
865
+ const plane = planeForCard(name);
866
+ const dest = join(root, '.xray', 'state', 'goggles-plane.json');
860
867
  mkdirSync(dirname(dest), { recursive: true });
861
- writeFileSync(dest, `${JSON.stringify({ paths: unique })}\n`);
868
+ writeFileSync(dest, `${JSON.stringify({ plane, card: name })}\n`);
869
+ maintainLens(root);
862
870
  }
863
871
 
864
- function lensRecords() {
872
+ function stampPlane(root, id) {
865
873
  try {
866
- const data = JSON.parse(readFileSync(planesFile(), 'utf8'));
867
- const records = [];
868
- if (!data || typeof data !== 'object') return records;
869
- for (const [name, entry] of Object.entries(data)) {
870
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) continue;
871
- if (!Array.isArray(entry.files)) continue;
872
- records.push([name, entry.files.filter((file) => typeof file === 'string')]);
873
- }
874
- return records;
874
+ const plates = createRequire(import.meta.url)('./plates.cjs');
875
+ if (!plates.PLATE_IDS.includes(id)) return;
876
+ plates.stampPlateIfMissing(root, id);
875
877
  } catch {
876
- return [];
878
+ /* a missed stamp must not fail the stop */
877
879
  }
878
880
  }
879
881
 
880
- function plateIsOnDisk(root, name) {
881
- const beside = findPlatesDir(HERE);
882
- if (beside && existsSync(join(beside, `${name}.md`))) return true;
883
- return existsSync(join(root, 'docs-site', 'docs', 'plates', `${name}.md`));
884
- }
885
-
886
- function lensCovers(root, file) {
887
- for (const [name, files] of lensRecords()) {
888
- if (!files.includes(file)) continue;
889
- if (name === 'ground' || plateIsOnDisk(root, name)) return true;
882
+ function openPaths(text) {
883
+ const raw = String(text || '').replace(/\\/g, '/');
884
+ const found = [];
885
+ const re = /(?:^|[^A-Za-z0-9_])((?:src|scripts|docs-site|\.agents|grok-bot|node_modules\/@0xray)\/[\w./@-]*|AGENTS\.md|package\.json|README\.md|\.mcp\.json)/g;
886
+ let match = re.exec(raw);
887
+ while (match) {
888
+ found.push(match[1].replace(/\/+$/, ''));
889
+ match = re.exec(raw);
890
890
  }
891
- return false;
891
+ return found;
892
892
  }
893
893
 
894
- function unsettledDebt(root) {
895
- const prior = loadDebt(root);
896
- const kept = prior.filter((file) => !lensCovers(root, file));
897
- if (kept.length !== prior.length) saveDebt(root, kept);
898
- return kept;
894
+ function isSearch(tool, text) {
895
+ const name = String(tool || '');
896
+ const spoken = String(text || '');
897
+ if (/researcher|explorer|deep[- ]?research/i.test(name) || DEEP_SEARCH.test(name)) return true;
898
+ if (/semanticsearch|codebase_search/i.test(name)) return true;
899
+ if (/^(grep|glob|rg|ripgrep)$/i.test(name)) return true;
900
+ return /bash|shell/i.test(name) && (shellSearches(spoken) || DEEP_SEARCH.test(spoken) || /researcher|explorer|deep[- ]?research/i.test(spoken));
899
901
  }
900
902
 
901
- function rememberOpened(root, text) {
902
- const fresh = openedSourcePaths(text).filter((file) => !lensCovers(root, file));
903
- if (!fresh.length) return;
904
- saveDebt(root, [...loadDebt(root), ...fresh]);
903
+ function isRead(tool, text) {
904
+ if (/^(read_file|read)$/i.test(String(tool || ''))) return true;
905
+ return /bash|shell/i.test(String(tool || '')) && shellReadsSource(text) && !shellSearches(text);
905
906
  }
906
907
 
907
- function debtReason(paths) {
908
- return `Opened ${paths.join(', ')} has no lens.`;
908
+ function currentPlane(root) {
909
+ const held = readHold(root);
910
+ if (held && CARD_PLANES.includes(held.plane)) return held.plane;
911
+ return 'dev';
909
912
  }
910
913
 
911
- function researchCall(toolName, text) {
912
- const tool = String(toolName || '');
913
- const spoken = String(text || '');
914
- if (/researcher|explorer|deep[- ]?research/i.test(tool) || DEEP_SEARCH.test(tool)) return true;
915
- if (/bash|shell/i.test(tool) && (/researcher|explorer|deep[- ]?research/i.test(spoken) || DEEP_SEARCH.test(spoken) || shellSearches(spoken) || shellReadsSource(spoken))) return true;
916
- if (/^(grep|glob)$/i.test(tool)) return true;
917
- return /^read_file$/i.test(tool) && !opensLensFile(spoken);
914
+ function currentCard(root) {
915
+ const held = readHold(root);
916
+ if (held && held.card) return held.card;
917
+ return currentPlane(root);
918
918
  }
919
919
 
920
- function passPath(root) {
921
- return join(root, '.xray', 'state', 'goggles-lens-pass.json');
920
+ function ensurePlane(root) {
921
+ const held = readHold(root);
922
+ if (held && CARD_PLANES.includes(held.plane)) return held.plane;
923
+ holdPlane(root, 'dev');
924
+ return 'dev';
922
925
  }
923
926
 
924
- function loadReadPass(root) {
925
- try {
926
- const data = JSON.parse(readFileSync(passPath(root), 'utf8'));
927
- if (!data || data.read !== true) return null;
928
- return data.spent === true ? 'spent' : 'armed';
929
- } catch {
930
- return null;
931
- }
927
+ function cardText(root, id) {
928
+ const card = presentCard(root, id);
929
+ return card && card.text ? card.text : '';
932
930
  }
933
931
 
934
- function saveReadPass(root, spent) {
935
- const file = passPath(root);
936
- mkdirSync(dirname(file), { recursive: true });
937
- writeFileSync(file, `${JSON.stringify({ read: true, spent: Boolean(spent) })}\n`);
932
+ /** The current card, for the boot payload. Starts on dev. */
933
+ export function planeCard(root) {
934
+ const plane = ensurePlane(root);
935
+ return { plane, card: cardText(root, currentCard(root)) };
938
936
  }
939
937
 
940
- /** One plane: the files the suit continues with. A look with no plane stops. Two planes stay quiet. */
941
- export function lensPlane(text) {
942
- const hint = suitHint(String(text || ''));
943
- if (hint === 'Name one plane.') return { stop: true, files: [] };
944
- if (!hint || hint.includes('\n')) return { stop: false, files: [] };
945
- const mark = hint.indexOf(': ');
946
- if (mark < 0) return { stop: false, files: [] };
947
- const files = hint.slice(mark + 2).split(',').map((part) => part.trim()).filter(Boolean);
948
- return { stop: false, files };
938
+
939
+ function rankedSkillRow(id) {
940
+ const builtin = PLANE_ROWS[id];
941
+ if (builtin && builtin.skills) return builtin.skills;
942
+ const row = overlayTable()[id];
943
+ return row && typeof row.skills === 'string' ? row.skills : '';
949
944
  }
950
945
 
951
- function lensPage(root, names) {
952
- if (!names.length) return 'Name one plane.';
953
- const platesDir = findPlatesDir(HERE);
954
- const parts = [];
955
- for (const id of names) {
956
- const plane = growPlane(id, platesDir, root);
957
- parts.push(formatCardText(cardView(plane, '', 'digest', sourceRoot(platesDir))));
946
+ function skillNamesOnCards() {
947
+ const names = new Set();
948
+ for (const row of Object.values(PLANE_ROWS)) {
949
+ for (const id of skillIdsFromRow(row.skills)) names.add(id);
950
+ }
951
+ for (const row of Object.values(overlayTable())) {
952
+ if (!row || typeof row.skills !== 'string') continue;
953
+ for (const id of skillIdsFromRow(row.skills)) names.add(id);
954
+ }
955
+ return names;
956
+ }
957
+
958
+ /** Replace a stored Skills row that still points at the catalog or one file. */
959
+ export function migrateLensSkills(root) {
960
+ const prev = readLensText(root);
961
+ if (!prev) return false;
962
+ const cards = lensCards(prev);
963
+ let next = prev;
964
+ let changed = false;
965
+ for (const [id, block] of cards) {
966
+ const current = cardRow(block, 'Skills');
967
+ const ranked = rankedSkillRow(id);
968
+ if (!ranked || !staleSkillRow(current) || current === ranked) continue;
969
+ const updated = block.replace(/^Skills:.*$/m, `Skills: ${ranked}`);
970
+ if (!next.includes(block)) continue;
971
+ next = next.replace(block, updated);
972
+ changed = true;
958
973
  }
959
- return parts.join('\n\n');
974
+ if (!changed) return false;
975
+ writeFileSync(lensPath(root), next.endsWith('\n') ? next : `${next}\n`);
976
+ return true;
977
+ }
978
+
979
+ /** Read the held card's skill list. Plate recall runs only when the list has no fit. */
980
+ export function seekHeldCard(root, need, opts = {}) {
981
+ const id = currentCard(root);
982
+ migrateLensSkills(root);
983
+ const shown = lookCards([id], root);
984
+ const skills = (shown && shown[0] && shown[0].skills) || rankedSkillRow(id);
985
+ const recall = typeof opts.recall === 'function'
986
+ ? opts.recall
987
+ : (text) => {
988
+ try {
989
+ return createRequire(import.meta.url)('./plates.cjs').recallPlate(text);
990
+ } catch {
991
+ return null;
992
+ }
993
+ };
994
+ return seekSkill({
995
+ skills,
996
+ need,
997
+ taskType: opts.taskType || '',
998
+ workKind: opts.workKind || '',
999
+ recall,
1000
+ known: skillNamesOnCards(),
1001
+ cardSkills: rankedSkillRow,
1002
+ });
960
1003
  }
961
1004
 
962
1005
 
963
- export function holdPlane(root, id) {
964
- const plane = String(id || "").trim();
965
- if (!plane || plane.includes(" ")) return;
966
- const dest = join(root, ".xray", "state", "goggles-plane.json");
967
- mkdirSync(dirname(dest), { recursive: true });
968
- writeFileSync(dest, `${JSON.stringify({ plane })}\n`);
969
- }
970
-
971
- function stampPlane(root, id) {
972
- try {
973
- const plates = createRequire(import.meta.url)('./plates.cjs');
974
- if (!plates.PLATE_IDS.includes(id)) return;
975
- plates.stampPlateIfMissing(root, id);
976
- } catch {
977
- /* a missed stamp must not fail the stop */
978
- }
1006
+ function targetOf(file) {
1007
+ const thing = thingOfPath(file);
1008
+ const plane = planeOfPath(file) || (thing ? planeForCard(thing) : '');
1009
+ if (!plane) return null;
1010
+ return { plane, card: thing || plane };
979
1011
  }
980
1012
 
981
- /** Name one plane and the tool continues. Name none, or more than one, and the search stops. */
1013
+ /** A search inside the current card runs. A file open follows the path. */
982
1014
  export function lensBeforeResearch(root, toolName, text) {
983
1015
  const spoken = String(text || '');
984
- const openDebt = unsettledDebt(root);
985
- const research = researchCall(toolName, spoken);
986
- if (openDebt.length && research) {
987
- return { gate: 'lens', decision: 'deny', reason: debtReason(openDebt) };
1016
+ const searching = isSearch(toolName, spoken);
1017
+ const reading = isRead(toolName, spoken);
1018
+ if (!searching && !reading) return null;
1019
+ ensurePlane(root);
1020
+ const current = currentCard(root);
1021
+ if (searching) {
1022
+ const owned = cardMatchPaths(current);
1023
+ const paths = openPaths(spoken);
1024
+ const inside = owned.length > 0
1025
+ && paths.length > 0
1026
+ && paths.every((file) => owned.some((listed) => covers(listed, file)));
1027
+ if (inside) return null;
1028
+ return { gate: 'lens', decision: 'deny', reason: cardText(root, current) };
988
1029
  }
989
- if (openDebt.length && isLensWrite(toolName) && !isStampPath(spoken)) {
990
- return { gate: 'lens', decision: 'deny', reason: debtReason(openDebt) };
1030
+ const targets = [];
1031
+ for (const file of openPaths(spoken)) {
1032
+ const target = targetOf(file);
1033
+ if (target && !targets.includes(target.card)) targets.push(target.card);
991
1034
  }
992
- if (!research) return null;
993
- const names = CARD_PLANES.filter((name) => intentWords(spoken).includes(name));
994
- if (names.length === 1) {
995
- holdPlane(root, names[0]);
996
- stampPlane(root, names[0]);
997
- rememberOpened(root, spoken);
998
- return null;
999
- }
1000
- if (/^read_file$/i.test(String(toolName || ''))) {
1001
- if (loadReadPass(root) === 'armed') {
1002
- saveReadPass(root, true);
1003
- return null;
1004
- }
1005
- if (loadReadPass(root) !== 'spent') saveReadPass(root, false);
1006
- return { gate: 'lens', decision: 'deny', reason: 'Name one plane.' };
1035
+ if (targets.length === 1 && targets[0] === current) return null;
1036
+ if (targets.length === 1) {
1037
+ holdPlane(root, targets[0]);
1038
+ return { gate: 'lens', decision: 'allow', reason: cardText(root, targets[0]) };
1007
1039
  }
1008
- return { gate: 'lens', decision: 'deny', reason: 'Name one plane.' };
1040
+ return { gate: 'lens', decision: 'allow', reason: cardText(root, current) };
1009
1041
  }
1010
1042
 
1011
1043
  function seenFields(plane, root) {
@@ -1052,15 +1084,11 @@ function freshField(seen, key) {
1052
1084
  return seen[key] || '';
1053
1085
  }
1054
1086
 
1055
- export function growPlane(id, platesDir, root) {
1087
+ export function growPlane(id, platesDir, _root) {
1056
1088
  const seen = seenFields(assemblePlane(id, platesDir), sourceRoot(platesDir));
1057
- const views = loadViews(root);
1058
1089
  const grown = { id };
1059
1090
  for (const key of VIEW_KEYS) grown[key] = freshField(seen, key);
1060
1091
  if (id === 'ground') grown.from = '';
1061
- views[id] = {};
1062
- for (const key of VIEW_KEYS) views[id][key] = grown[key];
1063
- saveViews(root, views);
1064
1092
  const base = assemblePlane(id, platesDir);
1065
1093
  return { ...base, ...grown };
1066
1094
  }
@@ -1069,72 +1097,10 @@ function repoRootFrom(platesDir) {
1069
1097
  return dirname(dirname(dirname(platesDir)));
1070
1098
  }
1071
1099
 
1072
- function miss(text) {
1073
- return { ok: false, text };
1074
- }
1075
-
1076
1100
  function ok(text) {
1077
1101
  return { ok: true, text };
1078
1102
  }
1079
1103
 
1080
- function holds() {
1081
- return { kind: 'holds', text: 'Holds.' };
1082
- }
1083
-
1084
- function drift(text) {
1085
- return { kind: 'drift', text: `Drift: ${text}` };
1086
- }
1087
-
1088
- function drawing() {
1089
- return { kind: 'drawing', text: 'Drawing only.' };
1090
- }
1091
-
1092
- function fileHas(root, rel, mark) {
1093
- if (!rel || !mark) return false;
1094
- const full = join(root, rel);
1095
- return existsSync(full) && readFileSync(full, 'utf8').includes(mark);
1096
- }
1097
-
1098
-
1099
- function factoryName(rel) {
1100
- return rel.startsWith('src/') || rel.startsWith('grok-bot/');
1101
- }
1102
-
1103
- function wornHolds(plane, root) {
1104
- if (!plane.worn || !existsSync(join(root, plane.worn))) return false;
1105
- if (!plane.wornMark) return true;
1106
- return fileHas(root, plane.worn, plane.wornMark);
1107
- }
1108
-
1109
- export function examinePlane(plane, root) {
1110
- const files = Array.isArray(plane.files) ? plane.files : [];
1111
- const checkable = files.length > 0 || plane.skills || plane.worn;
1112
- if (!checkable) return drawing();
1113
- const held = wornHolds(plane, root);
1114
- if (plane.worn && !held) {
1115
- return drift(`the worn build is not ${files[0] || plane.worn}`);
1116
- }
1117
- for (const rel of files) {
1118
- if (existsSync(join(root, rel))) continue;
1119
- if (held && factoryName(rel)) continue;
1120
- return drift(`${rel} is not there`);
1121
- }
1122
- if (plane.mark && files[0] && existsSync(join(root, files[0])) && !fileHas(root, files[0], plane.mark)) {
1123
- return drift(`${plane.mark} is not in ${files[0]}`);
1124
- }
1125
- if (plane.skills && !existsSync(join(root, plane.skills)) && !(held && factoryName(plane.skills))) {
1126
- return drift(`${plane.skills} is not there`);
1127
- }
1128
- if (plane.entryIsMark && plane.entry && files[0] && existsSync(join(root, files[0])) && !fileHas(root, files[0], plane.entry)) {
1129
- return drift(`${plane.entry} is not in ${files[0]}`);
1130
- }
1131
- if (plane.exitIsMark && plane.exit && files[0] && existsSync(join(root, files[0])) && !fileHas(root, files[0], plane.exit)) {
1132
- return drift(`${plane.exit} is not in ${files[0]}`);
1133
- }
1134
- return holds();
1135
- }
1136
-
1137
-
1138
1104
  export function look(argv, projectRoot) {
1139
1105
  return readGoggles(argv, projectRoot);
1140
1106
  }
@@ -1145,15 +1111,6 @@ function isGogglesWork(text) {
1145
1111
  }
1146
1112
 
1147
1113
 
1148
- export function cardStop(root, toolName, text, paths = []) {
1149
- const held = loadReading(root);
1150
- if (!held) return null;
1151
- const drift = actualityLine();
1152
- if (drift) return organDeny(drift);
1153
- const stopped = organStop(held, { tool: toolName, text, paths });
1154
- if (stopped) return stopped;
1155
- return { gate: 'goggles', decision: 'allow' };
1156
- }
1157
1114
 
1158
1115
  function stationFile(root) {
1159
1116
  return join(root, '.xray', 'state', 'STATION.md');
@@ -1188,35 +1145,10 @@ function lookVerb(words) {
1188
1145
  export function handCard(root, intent) {
1189
1146
  try {
1190
1147
  const text = String(intent || '');
1191
- const words = intentWords(text);
1192
1148
  if (isGogglesWork(text) && text.length <= 200) {
1193
1149
  clearReading(root);
1194
1150
  clearCardLine(root);
1195
- return null;
1196
1151
  }
1197
- const asked = lookVerb(words);
1198
- const planes = acceptedPlanes().filter((name) => words.includes(name));
1199
- if (!asked && planes.length === 0) return null;
1200
- if (!asked && planes.length === 1 && !HOLDS_ON_NAME.has(planes[0])) return null;
1201
- clearCardLine(root);
1202
- const looked = readGoggles(words);
1203
- if (looked.text.startsWith('Actuality. Drift')) {
1204
- saveReading(root, { drift: looked.text });
1205
- return null;
1206
- }
1207
- if (planes.length !== 1) {
1208
- clearReading(root);
1209
- return null;
1210
- }
1211
- const scopes = findScopes(words.filter((word) => word !== planes[0]));
1212
- if (scopes.length > 1) {
1213
- clearReading(root);
1214
- return null;
1215
- }
1216
- const next = { plane: planes[0], scope: scopes[0] || '' };
1217
- const prev = loadReading(root);
1218
- if (prev && prev.plane === next.plane && (prev.scope || '') === next.scope && !prev.drift) return null;
1219
- saveReading(root, next);
1220
1152
  return null;
1221
1153
  } catch {
1222
1154
  return null;
@@ -1229,12 +1161,115 @@ export function lensPath(root) {
1229
1161
  }
1230
1162
 
1231
1163
 
1232
- export function maintainLens(root) {
1233
- const drift = actualityLine();
1234
- const text = drift || 'quiet (match)';
1164
+ function heldCardPlane(root) {
1165
+ const held = readHold(root);
1166
+ if (!held || !held.card) return null;
1167
+ return { id: held.card, scope: '' };
1168
+ }
1169
+
1170
+ function readLensText(root) {
1171
+ try {
1172
+ return readFileSync(lensPath(root), 'utf8');
1173
+ } catch {
1174
+ return '';
1175
+ }
1176
+ }
1177
+
1178
+ function blockId(part) {
1179
+ const card = String(part || '').match(/^Card:\s*(\S+)/m);
1180
+ if (card) return card[1];
1181
+ const plane = String(part || '').match(/^Plane:\s*(\S+)/);
1182
+ return plane ? plane[1] : '';
1183
+ }
1184
+
1185
+ function lensCards(text) {
1186
+ const cards = new Map();
1187
+ for (const part of String(text || '').split(/\n\n/)) {
1188
+ const id = blockId(part);
1189
+ if (!id) continue;
1190
+ const block = part.replace(/\s*$/, '');
1191
+ if (!isCardName(id) && !cardIsFilled(block)) continue;
1192
+ cards.set(id, block);
1193
+ }
1194
+ return cards;
1195
+ }
1196
+
1197
+ function cardIsFilled(block) {
1198
+ return /^Entry:/m.test(block) && /^Exit:/m.test(block);
1199
+ }
1200
+
1201
+ function cardOrder(text, cards) {
1202
+ const order = [];
1203
+ for (const part of String(text || '').split(/\n\n/)) {
1204
+ const id = blockId(part);
1205
+ if (!id || !cards.has(id) || order.includes(id)) continue;
1206
+ order.push(id);
1207
+ }
1208
+ for (const id of cards.keys()) {
1209
+ if (!order.includes(id)) order.push(id);
1210
+ }
1211
+ return order;
1212
+ }
1213
+
1214
+ function writeLensBody(root, prev, cards) {
1215
+ const parts = [];
1216
+ for (const id of cardOrder(prev, cards)) parts.push(cards.get(id));
1235
1217
  mkdirSync(dirname(lensPath(root)), { recursive: true });
1236
- writeFileSync(lensPath(root), `${text}\n`);
1237
- return ok(drift);
1218
+ writeFileSync(lensPath(root), parts.length ? `${parts.join('\n\n')}\n` : '');
1219
+ }
1220
+
1221
+ function lensCardText(root, held) {
1222
+ const built = builtinCard(held.id);
1223
+ if (built) return built;
1224
+ const platesDir = findPlatesDir(HERE);
1225
+ const plane = growPlane(held.id, platesDir, root);
1226
+ const place = planeForCard(held.id);
1227
+ const view = {
1228
+ card: held.id,
1229
+ plane: place,
1230
+ from: place,
1231
+ digest: plane.digest || '',
1232
+ plate: plane.plate || '',
1233
+ plates: Array.isArray(plane.plates) ? plane.plates : [],
1234
+ entry: plane.entry || '',
1235
+ exit: plane.exit || '',
1236
+ files: listedFiles(plane),
1237
+ skills: plane.skills || '',
1238
+ setup: plane.setup || '',
1239
+ teardown: plane.teardown || '',
1240
+ worn: plane.worn || '',
1241
+ };
1242
+ return cardRows(view).map(([key, value]) => `${key}: ${value}`.trimEnd()).join('\n');
1243
+ }
1244
+
1245
+ /** The stored card. Empty until Entry and Exit are in the file. */
1246
+ export function readLensCard(root, id) {
1247
+ const block = lensCards(readLensText(root)).get(String(id || '').trim()) || '';
1248
+ return cardIsFilled(block) ? block : '';
1249
+ }
1250
+
1251
+ /** Write the plane card once. A later pass reads the stored rows. */
1252
+ export function fillLensCard(root, id) {
1253
+ const plane = String(id || '').trim();
1254
+ if (!isCardName(plane)) return '';
1255
+ const kept = readLensCard(root, plane);
1256
+ if (kept) return kept;
1257
+ const prev = readLensText(root);
1258
+ const cards = lensCards(prev);
1259
+ const block = lensCardText(root, { id: plane, scope: '' });
1260
+ cards.set(plane, block);
1261
+ writeLensBody(root, prev, cards);
1262
+ return block;
1263
+ }
1264
+
1265
+ export function maintainLens(root) {
1266
+ migrateLensSkills(root);
1267
+ const held = heldCardPlane(root);
1268
+ if (held) fillLensCard(root, held.id);
1269
+ const prev = readLensText(root);
1270
+ const cards = lensCards(prev);
1271
+ writeLensBody(root, prev, cards);
1272
+ return ok('');
1238
1273
  }
1239
1274
 
1240
1275
 
@@ -1248,7 +1283,7 @@ function sameInvoked(argvPath, modulePath) {
1248
1283
 
1249
1284
  const isMain = Boolean(process.argv[1]) && sameInvoked(process.argv[1], fileURLToPath(import.meta.url));
1250
1285
  if (isMain) {
1251
- const answered = gogglesAnswer(process.argv.slice(2));
1286
+ const answered = gogglesAnswer(process.argv.slice(2), undefined, true);
1252
1287
  const pane = process.stdout.isTTY && Array.isArray(answered.cards) && answered.cards.length > 0;
1253
1288
  const text = pane ? answered.cards.map((card) => formatCardPane(card)).join('\n\n') : answered.text;
1254
1289
  process.stdout.write(`${text}\n`);