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
@@ -179,10 +179,24 @@ function stampPlateIfMissing(projectRoot, id) {
179
179
  return { id, path, written: true };
180
180
  }
181
181
 
182
+ function organFile(intent) {
183
+ try {
184
+ const { suitHint } = require("./goggles-pipeline.mjs");
185
+ const hint = suitHint(String(intent || ""));
186
+ if (!hint || hint === "Name one plane." || hint.includes("\n")) return "";
187
+ const file = hint.split(": ").slice(1).join(": ").trim();
188
+ return file;
189
+ } catch {
190
+ return "";
191
+ }
192
+ }
193
+
182
194
  function plateStockLine(intent) {
183
195
  const plate = recallPlate(intent);
184
196
  if (!plate) return null;
185
- return `Plate: ${plate.id} — .xray/state/plates/${plate.id}.md`;
197
+ const line = `Plate: ${plate.id} — .xray/state/plates/${plate.id}.md`;
198
+ const file = organFile(intent);
199
+ return file ? `${line} File: ${file}` : line;
186
200
  }
187
201
 
188
202
  module.exports = {
@@ -127,16 +127,26 @@ function bootHeadMoved(root, existing) {
127
127
  return Boolean(live && live.head && bootHead && bootHead !== live.head);
128
128
  }
129
129
 
130
- /** Live ticket beats leftover boot. Extra spoken intent still wins. */
130
+ /** Live ticket beats leftover boot. Chat and compact do not replace it. */
131
131
  function resolveHeatIntent(root, extra, existing) {
132
132
  const incoming = clipIntent(
133
133
  extra.intent || extra.prompt || extra.userMessage || extra.user_prompt,
134
134
  );
135
- if (incoming) return { intent: incoming, rematch: true };
136
135
  const card = readStationTicketField(root, "Intent");
137
136
  const pickup = clipIntent(readNotesPickup(root));
138
137
  const bootIntent = typeof existing.intent === "string" ? clipIntent(existing.intent) : null;
139
138
  const cardIsBootEcho = Boolean(card && bootIntent && card === bootIntent);
139
+ const hook = String(extra.hookEvent || extra.source || "");
140
+ const keepsTicket = /user_prompt|compact/i.test(hook);
141
+ const compact = /compact/i.test(hook);
142
+ // A chat sentence or a compact summary does not replace a ticket already on disk.
143
+ if (incoming && !(keepsTicket && (card || pickup))) {
144
+ return { intent: incoming, rematch: true };
145
+ }
146
+ // Compact copies the card. A stale pickup must not overwrite it.
147
+ if (compact && card && card !== incoming) {
148
+ return { intent: card, rematch: card !== bootIntent };
149
+ }
140
150
  if (pickup && cardIsBootEcho && pickup !== bootIntent) {
141
151
  return { intent: pickup, rematch: true };
142
152
  }
@@ -490,6 +500,32 @@ function readNotesPickup(root) {
490
500
  }
491
501
  }
492
502
 
503
+ /** One pickup line. Leaves the rest of NOTES.md alone. */
504
+ function stampNotesPickup(root, line) {
505
+ const text = clipIntent(line);
506
+ if (!text) return null;
507
+ const dest = notesPath(root);
508
+ const marker = `**Pickup line:** ${text}`;
509
+ let body = "";
510
+ if (existsSync(dest)) {
511
+ try {
512
+ body = readFileSync(dest, "utf8");
513
+ } catch {
514
+ body = "";
515
+ }
516
+ }
517
+ const hasLine = /\*\*Pickup line:\*\*/.test(body);
518
+ const next = hasLine
519
+ ? body.replace(/\*\*Pickup line:\*\*\s*[^\n]*/, marker)
520
+ : body
521
+ ? `${marker}\n\n${body.replace(/^\n+/, "")}`
522
+ : `${marker}\n`;
523
+ if (next === body) return text;
524
+ mkdirSync(join(root, ".xray", "state"), { recursive: true });
525
+ writeFileSync(dest, next.endsWith("\n") ? next : `${next}\n`);
526
+ return text;
527
+ }
528
+
493
529
  function readLatestSessionApproaches(root) {
494
530
  const latest = join(root, "docs", "inference", "latest-session.json");
495
531
  if (!existsSync(latest)) return null;
@@ -838,6 +874,11 @@ function isCompactHook(extra) {
838
874
  return /compact/i.test(hook);
839
875
  }
840
876
 
877
+ function isPromptHook(extra) {
878
+ const hook = String((extra && (extra.hookEvent || extra.source)) || "");
879
+ return /user_prompt/i.test(hook);
880
+ }
881
+
841
882
  function isCompactEventName(value) {
842
883
  return /compact/i.test(String(value || ""));
843
884
  }
@@ -921,7 +962,8 @@ function buildRepertoireResume(root) {
921
962
  function applyStationHeat(root, host, extra = {}, existing = {}) {
922
963
  const mr = readMemoryRoutingConfig(root);
923
964
  const memoryOff = isExplicitMemoryRoutingOptOut(mr);
924
- const live = memoryOff
965
+ const promptHook = isPromptHook(extra);
966
+ const live = memoryOff || promptHook
925
967
  ? { hydrate: { dest: null, added: 0, destCount: 0 }, captured: null, grow: null }
926
968
  : withDestLock(root, () => {
927
969
  const hydrateInner = hydrateDestOnWake(root);
@@ -938,7 +980,7 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
938
980
  : existsSync(destSignalsPath(root))
939
981
  ? countCuratedSignals(destSignalsPath(root)) || 0
940
982
  : hydrate.destCount;
941
- const pickup = readNotesPickup(root);
983
+ let pickup = readNotesPickup(root);
942
984
  const approaches = readLatestSessionApproaches(root);
943
985
  const prevHost = typeof existing.host === "string" ? existing.host : null;
944
986
  const nextSwap = prevHost && host && prevHost !== host ? { from: prevHost, to: host } : null;
@@ -953,6 +995,10 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
953
995
  const resolved = resolveHeatIntent(root, extra, existing);
954
996
  const intent = resolved.intent;
955
997
  const rematch = resolved.rematch;
998
+ if (isCompactHook(extra) && intent && (readStationTicketField(root, "Intent") || pickup || clipIntent(extra.intent))) {
999
+ const stamped = stampNotesPickup(root, intent);
1000
+ if (stamped) pickup = stamped;
1001
+ }
956
1002
  const matchText = clipIntent([intent, pickup, approaches].filter(Boolean).join(" "));
957
1003
  const git = readGitBrief(root);
958
1004
  const planLine = resolveHeatPlan(root, extra, existing);
@@ -972,7 +1018,9 @@ function applyStationHeat(root, host, extra = {}, existing = {}) {
972
1018
  hydrate.destCount ||
973
1019
  (existsSync(destSignalsPath(root)) ? countCuratedSignals(destSignalsPath(root)) : 0);
974
1020
  if (!memoryOff && !matchedSignals.length && matchText) {
975
- if (
1021
+ if (promptHook) {
1022
+ matchedSignals = preferLawHits(priorWorking && priorWorking.matchedSignals, 8);
1023
+ } else if (
976
1024
  !rematch &&
977
1025
  priorWorking &&
978
1026
  priorWorking.matchText === matchText &&
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.31",
3
+ "version": "4.0.33",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -81,9 +81,14 @@ export class AgentCapabilitiesManager {
81
81
  selectAgentForTask(requiredCapabilities, complexity, operationDescription = '', taskType = '') {
82
82
  if (operationDescription.trim()) {
83
83
  const routed = scoreAndRoute(operationDescription, {});
84
+ if (routed.lens === 'Name one plane.')
85
+ return null;
84
86
  if (routed.memoryRouting?.overridden && this.capabilities.has(routed.agent)) {
85
87
  return routed.agent;
86
88
  }
89
+ if (routed.file && routed.agent && routed.agent !== 'researcher' && this.capabilities.has(routed.agent)) {
90
+ return routed.agent;
91
+ }
87
92
  }
88
93
  if (taskType.trim()) {
89
94
  const routed = routeSubagent(taskType);
@@ -22,10 +22,14 @@ export declare function scoreComplexity(operation: string, context: unknown, thr
22
22
  export declare function routeToAgent(scoreOrLevel: number | ComplexityScore, level?: ComplexityLevel): string;
23
23
  /**
24
24
  * Convenience: score first, then route — one call.
25
+ * One named plane: the route is that file, and the agent is not the researcher.
26
+ * A look that names no plane stops. Repertoire may still change the agent when a law matches.
25
27
  */
26
28
  export declare function scoreAndRoute(operation: string, context: unknown, thresholds?: ComplexityThresholds): {
27
29
  score: ComplexityScore;
28
30
  agent: string;
31
+ file?: string;
32
+ lens?: string;
29
33
  memoryRouting?: {
30
34
  providerId: string;
31
35
  adjustedScore: number;
@@ -7,6 +7,7 @@
7
7
  * Import from here or from delegation/index.ts (which re-exports these).
8
8
  */
9
9
  import { frameworkLogger } from "../core/framework-logger.js";
10
+ import { lensPlane } from "../integrations/hooks/goggles-pipeline.mjs";
10
11
  import { getLevelFromScore, getAgentForTier, getAgentCountForLevel, getStrategyForLevel, levelToTier, } from "../delegation/complexity-core.js";
11
12
  import { ComplexityAnalyzer } from "../delegation/complexity-analyzer.js";
12
13
  import { ensureMemoryRoutingProviderSync, initializeMemoryRouting, } from "../memory-routing/index.js";
@@ -54,9 +55,15 @@ export function routeToAgent(scoreOrLevel, level) {
54
55
  }
55
56
  /**
56
57
  * Convenience: score first, then route — one call.
58
+ * One named plane: the route is that file, and the agent is not the researcher.
59
+ * A look that names no plane stops. Repertoire may still change the agent when a law matches.
57
60
  */
58
61
  export function scoreAndRoute(operation, context, thresholds) {
59
62
  const score = scoreComplexity(operation, context, thresholds);
63
+ const plane = lensPlane(operation);
64
+ if (plane.stop) {
65
+ return { score, agent: "", lens: "Name one plane." };
66
+ }
60
67
  const baseAgent = routeToAgent(score);
61
68
  let agent = baseAgent;
62
69
  const provider = ensureMemoryRoutingProviderSync();
@@ -90,8 +97,9 @@ export function scoreAndRoute(operation, context, thresholds) {
90
97
  estimatedAgents: getAgentCountForLevel(adjustedLevel),
91
98
  },
92
99
  agent,
100
+ ...(plane.files[0] ? { file: plane.files[0] } : {}),
93
101
  memoryRouting,
94
102
  };
95
103
  }
96
- return { score, agent };
104
+ return { score, agent, ...(plane.files[0] ? { file: plane.files[0] } : {}) };
97
105
  }
@@ -55,15 +55,15 @@ When `multi_agent_orchestration.lead_dev_mode` is true in `features.json` (defau
55
55
 
56
56
  ## Goggles
57
57
 
58
- Pushed on `feat/goggles-look`. Not merged. Not published. Grok CLI has not shipped this. Wear the local build. From the worn checkout run `node dist/integrations/hooks/goggles-pipeline.mjs`. Do not run `npx 0xray goggles`. If `dist` is older than `src/integrations/hooks/goggles-pipeline.mjs`, build that tree and wear that `dist`. A subagent wears this same local build.
58
+ The command is `0xray look`. `0xray goggles` is the same organ. A pipe keeps the line card. A terminal shows that card as a pane. Chat seats without hooks use the `goggles` MCP tools `look` and `status_lens`. Same organ.
59
59
 
60
- Kind 0 is actuality. It compares the list in the file to the same six names in the code. It speaks only when those lists differ. A match is quiet. It does not read the suit that is running. It is not a plate type.
60
+ Kind 0 is actuality. It compares the list in the file to the same six names in the code. It speaks only when those lists differ. A match is quiet.
61
61
 
62
- Kind 1 names one plane: dichotomy, syncopate, synthesis, or loop. The lens holds that plane. An action on that plane is quiet. An action on another plane, or on a pipeline drawing, is stopped. A pipeline drawing is not a scope stop. Ground is not a scope stop. Scope, when named, is ecosystem, part, one flow, or one artifact, and an action wider than that scope is stopped. One kind and one scope level at a time. Two planes, calling, ground, and a pipeline name do not arm the lens. `outer` without one plane is `Name one plane.` After 1 is empty. Nothing is picked. Calling stays off.
62
+ Kind 1 names one plane: dichotomy, syncopate, synthesis, or loop. The lens holds that plane. An action on that plane is quiet. An action on another plane, or on a pipeline drawing, is stopped. A pipeline drawing is not a scope stop. Ground is not a scope stop. Scope, when named, is ecosystem, part, one flow, or one artifact, and an action wider than that scope is stopped. One kind and one scope level at a time. Two planes, calling, ground, and a pipeline name do not arm the lens. `outer` without one plane is `Name one plane.` After 1 is empty. Nothing is picked.
63
63
 
64
- `digest` and `triage` return the card already built by `assemblePlane`. The fields are from, digest, plate, entry, exit, files, skills, setup, teardown, and worn. Empty stays empty. The rows are ground, routing, house, boot, governance, memory-recall, orchestration, processor, and reporting. Those nine are not Kind 0. `digest` fills the card. Entry and exit are the plate's input and output box titles when that plate has them. Setup and teardown stay empty unless the row names them. The mill path is `scripts/foundry`. `triage` adds the empty fields and whether the row holds. Zoom after the card is the same ladder: ecosystem, part, one flow, one artifact. One file is one artifact. Several files are not narrowed to a guessed file.
64
+ Naming a plane, `digest`, or `triage` returns that plane's view. The fields are from, digest, plate, entry, exit, files, skills, setup, teardown, and worn. Each field has a pipe. The pipe writes what it can see from the plate or the tree, and the view keeps that field. A field the pipe cannot see yet stays on the view for the next look. The rows are ground, routing, house, boot, governance, memory-recall, orchestration, processor, and reporting. Those nine are not Kind 0. The mill path is `scripts/foundry`. `triage` names the fields still open and whether the row holds. Zoom after the card is the same ladder: ecosystem, part, one flow, one artifact.
65
65
 
66
- The hold lasts for the work that named the plane. It does not stop one tool and then forget. Work that says goggles clears the hold and stays quiet. The suit does not invent the plane.
66
+ The hold lasts for the work that named the plane. A sentence that does not say look or name does not arm it and does not drop it. Work that says goggles clears the hold and stays quiet. When a tool names one card plane, the suit adds that plane's file as a hint.
67
67
 
68
68
  ## Hooks (rewired, not new MCPs)
69
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.0.31",
3
+ "version": "4.0.33",
4
4
  "description": "Multi-agent orchestration and Codex governance for OpenCode, Hermes, Grok Build, and OpenClaw",
5
5
  "readme": "README.md",
6
6
  "license": "MIT",
@@ -170,6 +170,7 @@
170
170
  "scripts/mjs/verify-temperament-live.mjs",
171
171
  "scripts/mjs/verify-four-floor-compact.mjs",
172
172
  "scripts/mjs/run-repertoire-mcp.mjs",
173
+ "scripts/mjs/run-goggles-mcp.mjs",
173
174
  "scripts/mjs/verify-delegation-gate-core.mjs",
174
175
  "scripts/mjs/delegation-gate-fixture.mjs",
175
176
  "src/opencode/",
@@ -29,6 +29,10 @@ export const REQUIRED_PACK_PATHS = [
29
29
  "dist/integrations/cursor/hooks/pre-compact.sh",
30
30
  "dist/integrations/cursor/hooks/after-file-edit.sh",
31
31
  "dist/integrations/hooks/plates.cjs",
32
+ "dist/integrations/hooks/goggles-pipeline.mjs",
33
+ "dist/integrations/hooks/goggles-planes.json",
34
+ "dist/integrations/hooks/goggles-mcp.mjs",
35
+ "scripts/mjs/run-goggles-mcp.mjs",
32
36
  "docs-site/docs/plates/memory-recall.md",
33
37
  "docs-site/docs/plates/processor.md",
34
38
  "docs-site/docs/plates/goggles.md",
@@ -335,8 +335,13 @@ function isDirectory(p) {
335
335
  }
336
336
  }
337
337
 
338
- /** Passwd home. `os.homedir()` follows $HOME and cannot detect last-mile isolation. */
338
+ /**
339
+ * Passwd home. `os.homedir()` follows $HOME and cannot detect last-mile isolation.
340
+ * Tests set FOUNDRY_MACHINE_HOME so a plugin on the real machine does not fail a seat inspect.
341
+ */
339
342
  function machineHome() {
343
+ const override = process.env.FOUNDRY_MACHINE_HOME;
344
+ if (typeof override === "string" && override.trim()) return path.resolve(override.trim());
340
345
  try {
341
346
  const passwd = os.userInfo().homedir;
342
347
  if (typeof passwd === "string" && passwd) return passwd;
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CWD-proof Goggles MCP launcher.
4
+ * Resolves the organ server from this file, not from the caller's cwd.
5
+ */
6
+ import { spawn } from 'node:child_process';
7
+ import { existsSync } from 'node:fs';
8
+ import { dirname, join, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const xrayRoot = join(dirname(fileURLToPath(import.meta.url)), '../..');
12
+ const candidates = [
13
+ join(xrayRoot, 'dist', 'integrations', 'hooks', 'goggles-mcp.mjs'),
14
+ join(xrayRoot, 'src', 'integrations', 'hooks', 'goggles-mcp.mjs'),
15
+ ];
16
+ const server = candidates.find((file) => existsSync(file));
17
+ if (!server) {
18
+ process.stderr.write('goggles MCP missing\n');
19
+ process.exit(1);
20
+ }
21
+
22
+ const env = { ...process.env };
23
+ if (!env.GOGGLES_ROOT || env.GOGGLES_ROOT === '.') {
24
+ env.GOGGLES_ROOT = resolve(process.cwd());
25
+ }
26
+
27
+ const child = spawn(process.execPath, [server], {
28
+ stdio: 'inherit',
29
+ env,
30
+ });
31
+ child.on('error', (err) => {
32
+ process.stderr.write(`goggles MCP spawn failed: ${err.message}\n`);
33
+ process.exit(1);
34
+ });
35
+ child.on('exit', (code, signal) => {
36
+ if (signal) {
37
+ process.kill(process.pid, signal);
38
+ return;
39
+ }
40
+ process.exit(code ?? 1);
41
+ });
@@ -139,31 +139,70 @@ function enableMemoryRoutingIfResolves(features, targetDir) {
139
139
  };
140
140
  }
141
141
 
142
+ function resolveGogglesMcp(targetDir) {
143
+ const candidates = [
144
+ path.join(targetDir, "node_modules", "0xray", "scripts", "mjs", "run-goggles-mcp.mjs"),
145
+ path.join(targetDir, "scripts", "mjs", "run-goggles-mcp.mjs"),
146
+ ];
147
+ return candidates.find((file) => fs.existsSync(file)) || null;
148
+ }
149
+
150
+ function gogglesScopedLaunch(targetDir, launcher) {
151
+ return wrapScopedLaunch(
152
+ { command: "node", args: [launcher] },
153
+ mergeScopedEnv([
154
+ baseScopedEnv(targetDir),
155
+ { GOGGLES_ROOT: targetDir, GOGGLES_PLANES_PATH: "" },
156
+ ]),
157
+ );
158
+ }
159
+
142
160
  function detectConsumerExtraMcpServers(targetDir) {
143
161
  const extras = { hermes: {}, opencode: {}, openclaw: {} };
144
162
  try {
145
163
  const repertoireMcp = resolveRepertoireMcp(targetDir);
146
- if (!repertoireMcp) return extras;
147
- const launch = wrapScopedLaunch(
148
- { command: "node", args: [repertoireMcp] },
149
- baseScopedEnv(targetDir),
150
- );
151
- extras.hermes.repertoire = {
152
- command: launch.command,
153
- args: launch.args,
154
- env: launch.env,
155
- };
156
- extras.opencode.repertoire = {
157
- type: "local",
158
- command: launch.commandList,
159
- enabled: true,
160
- environment: launch.env,
161
- };
162
- extras.openclaw.repertoire = {
163
- command: launch.command,
164
- args: launch.args,
165
- env: launch.env,
166
- };
164
+ if (repertoireMcp) {
165
+ const launch = wrapScopedLaunch(
166
+ { command: "node", args: [repertoireMcp] },
167
+ baseScopedEnv(targetDir),
168
+ );
169
+ extras.hermes.repertoire = {
170
+ command: launch.command,
171
+ args: launch.args,
172
+ env: launch.env,
173
+ };
174
+ extras.opencode.repertoire = {
175
+ type: "local",
176
+ command: launch.commandList,
177
+ enabled: true,
178
+ environment: launch.env,
179
+ };
180
+ extras.openclaw.repertoire = {
181
+ command: launch.command,
182
+ args: launch.args,
183
+ env: launch.env,
184
+ };
185
+ }
186
+ const gogglesMcp = resolveGogglesMcp(targetDir);
187
+ if (gogglesMcp) {
188
+ const launch = gogglesScopedLaunch(targetDir, gogglesMcp);
189
+ extras.hermes.goggles = {
190
+ command: launch.command,
191
+ args: launch.args,
192
+ env: launch.env,
193
+ };
194
+ extras.opencode.goggles = {
195
+ type: "local",
196
+ command: launch.commandList,
197
+ enabled: true,
198
+ environment: launch.env,
199
+ };
200
+ extras.openclaw.goggles = {
201
+ command: launch.command,
202
+ args: launch.args,
203
+ env: launch.env,
204
+ };
205
+ }
167
206
  } catch {
168
207
  // best-effort
169
208
  }
@@ -880,6 +919,7 @@ module.exports = {
880
919
  detectConsumerExtraMcpServers,
881
920
  isRepertoirePackageRoot,
882
921
  resolveRepertoireMcp,
922
+ resolveGogglesMcp,
883
923
  resolveRepertoireProvider,
884
924
  isDefaultMemoryRoutingOff,
885
925
  enableMemoryRoutingIfResolves,
@@ -1580,7 +1580,7 @@ function ensureWearStateGitignored(targetDir) {
1580
1580
  return;
1581
1581
  }
1582
1582
  if (!top || !excludeRel) return;
1583
- const rel = path.relative(top, stateDir).split(path.sep).join("/");
1583
+ const rel = path.relative(realPath(top), realPath(stateDir)).split(path.sep).join("/");
1584
1584
  if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return;
1585
1585
  const pattern = `${rel}/`;
1586
1586
  const excludeFile = path.resolve(targetDir, excludeRel);
@@ -1817,6 +1817,22 @@ function saveWearState(paths, meta) {
1817
1817
  fs.writeFileSync(paths.meta, `${JSON.stringify(meta, null, 2)}\n`);
1818
1818
  }
1819
1819
 
1820
+ /** git prints the physical path. Node's tmpdir is the /var symlink. Compare the same file. */
1821
+ function realPath(p) {
1822
+ const abs = path.resolve(p);
1823
+ const missing = [];
1824
+ let cur = abs;
1825
+ while (cur && cur !== path.dirname(cur)) {
1826
+ try {
1827
+ return path.join(fs.realpathSync(cur), ...missing.reverse());
1828
+ } catch {
1829
+ missing.push(path.basename(cur));
1830
+ cur = path.dirname(cur);
1831
+ }
1832
+ }
1833
+ return abs;
1834
+ }
1835
+
1820
1836
  function resolveGitToplevel(targetDir) {
1821
1837
  try {
1822
1838
  const top = execFileSync("git", ["rev-parse", "--show-toplevel"], {
@@ -1825,8 +1841,8 @@ function resolveGitToplevel(targetDir) {
1825
1841
  stdio: ["ignore", "pipe", "ignore"],
1826
1842
  }).trim();
1827
1843
  if (!top) return null;
1828
- const resolved = path.resolve(top);
1829
- if (resolved === path.resolve(targetDir)) return null;
1844
+ const resolved = realPath(top);
1845
+ if (resolved === realPath(targetDir)) return null;
1830
1846
  return resolved;
1831
1847
  } catch {
1832
1848
  return null;
@@ -1835,11 +1851,12 @@ function resolveGitToplevel(targetDir) {
1835
1851
 
1836
1852
  function linkedWearRoots(targetDir) {
1837
1853
  const roots = [];
1854
+ const here = realPath(targetDir);
1838
1855
  const add = (root) => {
1839
1856
  if (!root) return;
1840
- const resolved = path.resolve(root);
1841
- if (resolved === path.resolve(targetDir)) return;
1842
- if (roots.some((item) => path.resolve(item) === resolved)) return;
1857
+ const resolved = realPath(root);
1858
+ if (resolved === here) return;
1859
+ if (roots.some((item) => realPath(item) === resolved)) return;
1843
1860
  roots.push(resolved);
1844
1861
  };
1845
1862
  add(resolveCursorWorkspaceRoot(targetDir));
@@ -2046,7 +2063,7 @@ function assertSetupGit(targetDir) {
2046
2063
  } catch {
2047
2064
  throw new Error("cursor-wear: not a git work tree");
2048
2065
  }
2049
- if (path.resolve(top) !== path.resolve(targetDir)) {
2066
+ if (realPath(top) !== realPath(targetDir)) {
2050
2067
  throw new Error("setup: work tree points elsewhere");
2051
2068
  }
2052
2069
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xray-pre-tool",
3
- "version": "4.0.31",
3
+ "version": "4.0.33",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "description": "0xRay constitution + temperament PreToolUse for OpenClaw",
@@ -105,9 +105,13 @@ export class AgentCapabilitiesManager {
105
105
  ): string | null {
106
106
  if (operationDescription.trim()) {
107
107
  const routed = scoreAndRoute(operationDescription, {});
108
+ if (routed.lens === 'Name one plane.') return null;
108
109
  if (routed.memoryRouting?.overridden && this.capabilities.has(routed.agent)) {
109
110
  return routed.agent;
110
111
  }
112
+ if (routed.file && routed.agent && routed.agent !== 'researcher' && this.capabilities.has(routed.agent)) {
113
+ return routed.agent;
114
+ }
111
115
  }
112
116
 
113
117
  if (taskType.trim()) {
@@ -55,15 +55,15 @@ When `multi_agent_orchestration.lead_dev_mode` is true in `features.json` (defau
55
55
 
56
56
  ## Goggles
57
57
 
58
- Pushed on `feat/goggles-look`. Not merged. Not published. Grok CLI has not shipped this. Wear the local build. From the worn checkout run `node dist/integrations/hooks/goggles-pipeline.mjs`. Do not run `npx 0xray goggles`. If `dist` is older than `src/integrations/hooks/goggles-pipeline.mjs`, build that tree and wear that `dist`. A subagent wears this same local build.
58
+ The command is `0xray look`. `0xray goggles` is the same organ. A pipe keeps the line card. A terminal shows that card as a pane. Chat seats without hooks use the `goggles` MCP tools `look` and `status_lens`. Same organ.
59
59
 
60
- Kind 0 is actuality. It compares the list in the file to the same six names in the code. It speaks only when those lists differ. A match is quiet. It does not read the suit that is running. It is not a plate type.
60
+ Kind 0 is actuality. It compares the list in the file to the same six names in the code. It speaks only when those lists differ. A match is quiet.
61
61
 
62
- Kind 1 names one plane: dichotomy, syncopate, synthesis, or loop. The lens holds that plane. An action on that plane is quiet. An action on another plane, or on a pipeline drawing, is stopped. A pipeline drawing is not a scope stop. Ground is not a scope stop. Scope, when named, is ecosystem, part, one flow, or one artifact, and an action wider than that scope is stopped. One kind and one scope level at a time. Two planes, calling, ground, and a pipeline name do not arm the lens. `outer` without one plane is `Name one plane.` After 1 is empty. Nothing is picked. Calling stays off.
62
+ Kind 1 names one plane: dichotomy, syncopate, synthesis, or loop. The lens holds that plane. An action on that plane is quiet. An action on another plane, or on a pipeline drawing, is stopped. A pipeline drawing is not a scope stop. Ground is not a scope stop. Scope, when named, is ecosystem, part, one flow, or one artifact, and an action wider than that scope is stopped. One kind and one scope level at a time. Two planes, calling, ground, and a pipeline name do not arm the lens. `outer` without one plane is `Name one plane.` After 1 is empty. Nothing is picked.
63
63
 
64
- `digest` and `triage` return the card already built by `assemblePlane`. The fields are from, digest, plate, entry, exit, files, skills, setup, teardown, and worn. Empty stays empty. The rows are ground, routing, house, boot, governance, memory-recall, orchestration, processor, and reporting. Those nine are not Kind 0. `digest` fills the card. Entry and exit are the plate's input and output box titles when that plate has them. Setup and teardown stay empty unless the row names them. The mill path is `scripts/foundry`. `triage` adds the empty fields and whether the row holds. Zoom after the card is the same ladder: ecosystem, part, one flow, one artifact. One file is one artifact. Several files are not narrowed to a guessed file.
64
+ Naming a plane, `digest`, or `triage` returns that plane's view. The fields are from, digest, plate, entry, exit, files, skills, setup, teardown, and worn. Each field has a pipe. The pipe writes what it can see from the plate or the tree, and the view keeps that field. A field the pipe cannot see yet stays on the view for the next look. The rows are ground, routing, house, boot, governance, memory-recall, orchestration, processor, and reporting. Those nine are not Kind 0. The mill path is `scripts/foundry`. `triage` names the fields still open and whether the row holds. Zoom after the card is the same ladder: ecosystem, part, one flow, one artifact.
65
65
 
66
- The hold lasts for the work that named the plane. It does not stop one tool and then forget. Work that says goggles clears the hold and stays quiet. The suit does not invent the plane.
66
+ The hold lasts for the work that named the plane. A sentence that does not say look or name does not arm it and does not drop it. Work that says goggles clears the hold and stays quiet. When a tool names one card plane, the suit adds that plane's file as a hint.
67
67
 
68
68
  ## Hooks (rewired, not new MCPs)
69
69
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./features.schema.json",
3
- "version": "4.0.31",
3
+ "version": "4.0.33",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",