0xray 4.0.30 → 4.0.32

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 (49) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.mcp.json +8 -0
  3. package/.opencode/skills/orchestrator/SKILL.md +12 -0
  4. package/.xray/features.json +1 -1
  5. package/dist/CHANGELOG.md +17 -0
  6. package/dist/cli/commands/status.d.ts +4 -2
  7. package/dist/cli/commands/status.js +6 -3
  8. package/dist/cli/commands/validate.js +9 -0
  9. package/dist/cli/goggles-status.d.ts +13 -0
  10. package/dist/cli/goggles-status.js +76 -0
  11. package/dist/cli/index.js +41 -2
  12. package/dist/integrations/grok/grok-cli.d.ts +3 -0
  13. package/dist/integrations/grok/grok-cli.js +41 -1
  14. package/dist/integrations/grok/hooks/grok-hook-utils.js +24 -4
  15. package/dist/integrations/grok/hooks/post-tool-use.js +2 -1
  16. package/dist/integrations/grok/hooks/pre-tool-use.js +90 -5
  17. package/dist/integrations/grok/hooks/session-start.js +7 -2
  18. package/dist/integrations/hooks/goggles-mcp.mjs +371 -0
  19. package/dist/integrations/hooks/goggles-pipeline.mjs +632 -0
  20. package/dist/integrations/hooks/goggles-planes.json +28 -0
  21. package/dist/integrations/hooks/plates.cjs +23 -0
  22. package/dist/integrations/hooks/station-hook-runtime.cjs +40 -11
  23. package/dist/integrations/hooks/station-memory-ingest.mjs +3 -0
  24. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  25. package/dist/mcps/orchestrator/config/agent-capabilities.js +7 -0
  26. package/dist/memory-routing/plates.d.ts +1 -1
  27. package/dist/memory-routing/plates.js +5 -0
  28. package/dist/memory-routing/provider-loader.js +2 -0
  29. package/dist/nucleus/thin-dispatch.d.ts +1 -0
  30. package/dist/nucleus/thin-dispatch.js +4 -2
  31. package/dist/skills/orchestrator/SKILL.md +12 -0
  32. package/docs-site/docs/plates/glossary.md +27 -0
  33. package/docs-site/docs/plates/goggles.md +18 -0
  34. package/docs-site/docs/plates/grokbot.md +41 -0
  35. package/docs-site/docs/plates/host-pack.md +32 -0
  36. package/docs-site/docs/plates/house.md +42 -39
  37. package/docs-site/docs/plates/index.md +13 -1
  38. package/docs-site/docs/plates/kits.md +28 -0
  39. package/docs-site/docs/plates/suit.md +40 -0
  40. package/package.json +2 -1
  41. package/scripts/foundry/assert-packed-dist-cli.mjs +9 -0
  42. package/scripts/mjs/run-goggles-mcp.mjs +41 -0
  43. package/scripts/node/bridge-mcp-wiring.cjs +61 -21
  44. package/scripts/node/consumer-install-smoke.mjs +5 -0
  45. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  46. package/src/mcps/orchestrator/config/agent-capabilities.ts +8 -0
  47. package/src/skills/orchestrator/SKILL.md +12 -0
  48. package/vendor/@0xray/repertoire/dist/paths.js +4 -0
  49. package/xray/features.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0xray",
3
- "version": "4.0.30",
3
+ "version": "4.0.32",
4
4
  "description": "0xRay suit for Grok, Hermes, OpenClaw, OpenCode: mill+inspect, Codex, 7 MCP servers. Costume 45/42 is opt-in foundry, not the default wear.",
5
5
  "author": {
6
6
  "name": "0xRay AI",
package/.mcp.json CHANGED
@@ -34,6 +34,14 @@
34
34
  "repertoire": {
35
35
  "command": "node",
36
36
  "args": ["scripts/mjs/run-repertoire-mcp.mjs"]
37
+ },
38
+ "goggles": {
39
+ "command": "node",
40
+ "args": ["scripts/mjs/run-goggles-mcp.mjs"],
41
+ "env": {
42
+ "GOGGLES_ROOT": ".",
43
+ "GOGGLES_PLANES_PATH": ""
44
+ }
37
45
  }
38
46
  }
39
47
  }
@@ -53,6 +53,18 @@ When `multi_agent_orchestration.lead_dev_mode` is true in `features.json` (defau
53
53
  | Test failures | bug-triage |
54
54
  | Review | code-review |
55
55
 
56
+ ## Goggles
57
+
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
+
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.
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.
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.
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.
67
+
56
68
  ## Hooks (rewired, not new MCPs)
57
69
 
58
70
  - **SessionStart** → `session-start.js` boots lead_dev_mode
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./features.schema.json",
3
- "version": "4.0.30",
3
+ "version": "4.0.32",
4
4
  "description": "0xRay Framework - Unified Feature Configuration",
5
5
  "suit_temperament": {
6
6
  "profile": "auto",
package/dist/CHANGELOG.md CHANGED
@@ -7,6 +7,7 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
7
7
  ## [Unreleased]
8
8
 
9
9
  ### Features
10
+ - **Goggles.** Proposed plates for goggles, suit, kits, host pack, and glossary. Goggles views one plane. Recall opens a plate when speech names that plate. Plates only. Not a new skill.
10
11
  - **Plates.** Pipeline schematics live in `docs-site/docs/plates/` (routing, governance, boot, orchestration, processor, reporting, memory-recall). Station keeps one pointer line. Pre-compact and the `MEMORY_ROUTING` inject carry that one schematic. A lesson is an episode. A plate is the machine. Not a new skill.
11
12
  - **Vendored Repertoire 0.2.8.** `vendor/@0xray/repertoire` is that package. Lesson lines and retained ids stay at 20. Speech that names nothing mints a learned signal, and only those mints evict past 24. Not a new skill.
12
13
  - **Feat rebase.** When the mandated work turns into mantra, or the job has split across planes that no longer share one present, stop and answer the original message again from this seat. On `LEAD-CADENCE.md`, the orchestrator skill, and the ship-ready mill gate. Project dest grows the name `feat-rebase` from the stack overlay. Not a new skill.
@@ -41,6 +42,22 @@ The format is based on [Conventional Commits](https://www.conventionalcommits.or
41
42
  - Archive root `integrations/` costume SKILL dump → `docs/archive/integrations-costume/`. Live wear is `src/integrations`.
42
43
  - Archive root `commands/` stale copy → `docs/archive/commands-copy/`. Live wear is `src/opencode/commands`.
43
44
 
45
+ ## [4.0.32] - 2026-10-01
46
+
47
+ Next patch after npm `4.0.31`. Not published from this commit.
48
+
49
+ Organ bolster for goggles-pipeline after #167. `0xray look` (and `goggles`) opens one plane through the organ. Digest and triage return the card pane. Status and health show the organ worn and Kind 0 quiet/match. Bot Plugin MCP `goggles` exposes lean tools `look` and `status_lens` over stdio on the same organ. Kind 0 name-checks the six outers and stays quiet on match; reading stays a dichotomy; digest/triage stay the card. Calling and confer stay off. Companion `@0xray/grok-bot` is unchanged at 0.1.8.
50
+
51
+ ---
52
+
53
+ ## [4.0.31] - 2026-10-01
54
+
55
+ Next patch after npm `4.0.30`. Not published from this commit.
56
+
57
+ Goggles looks at one pipeline. Kind 0 name-checks the six outer names and stays quiet when the worn list matches the map. Digest and triage return one card. A leave is denied. Session boot hands the card through `handCard` and normalizes the workspace root, including a trailing slash. The mill path is `scripts/foundry`. Run `node dist/integrations/hooks/goggles-pipeline.mjs`. This cut also carries the plate docs and the live project-file signal count that landed on main after 4.0.30. Calling and confer stay off. Companion `@0xray/grok-bot` is unchanged at 0.1.8.
58
+
59
+ ---
60
+
44
61
  ## [4.0.30] - 2026-09-28
45
62
 
46
63
  Next patch after npm `4.0.29`. Released 2026-09-28.
@@ -10,6 +10,7 @@
10
10
  *
11
11
  * Usage: npx 0xray status
12
12
  */
13
+ import type { GogglesStatus } from "../goggles-status.js";
13
14
  interface StatusReport {
14
15
  opencode: {
15
16
  installed: boolean;
@@ -34,8 +35,9 @@ interface StatusReport {
34
35
  outcomesCount: number;
35
36
  patternsCount: number;
36
37
  };
38
+ goggles: GogglesStatus;
37
39
  }
38
- export declare function getStatusReport(cwd?: string): StatusReport;
40
+ export declare function getStatusReport(cwd?: string, packageRoot?: string): StatusReport;
39
41
  export declare function printStatus(report: StatusReport): void;
40
- export declare function statusCommand(): Promise<void>;
42
+ export declare function statusCommand(packageRoot?: string): Promise<void>;
41
43
  export default statusCommand;
@@ -14,6 +14,7 @@ import { readdirSync, existsSync, readFileSync } from "fs";
14
14
  import { join } from "path";
15
15
  import { getConfigDir } from "../../core/config-paths.js";
16
16
  import { featuresConfigLoader } from "../../core/features-config.js";
17
+ import { formatGogglesStatus, probeGoggles } from "../goggles-status.js";
17
18
  function getSkillsList(cwd) {
18
19
  const skills = [];
19
20
  const configDir = getConfigDir(cwd);
@@ -119,7 +120,7 @@ function getInferenceStatus(cwd) {
119
120
  catch { /* ignore */ }
120
121
  return { active, lastTuning, outcomesCount, patternsCount };
121
122
  }
122
- export function getStatusReport(cwd = process.cwd()) {
123
+ export function getStatusReport(cwd = process.cwd(), packageRoot = cwd) {
123
124
  const opencodeConfigPath = join(cwd, "opencode.json");
124
125
  const cwdSkills = getSkillsList(cwd);
125
126
  const agents = getAgentsList(cwd);
@@ -134,6 +135,7 @@ export function getStatusReport(cwd = process.cwd()) {
134
135
  agents,
135
136
  health,
136
137
  inference,
138
+ goggles: probeGoggles(packageRoot, cwd),
137
139
  };
138
140
  }
139
141
  export function printStatus(report) {
@@ -166,6 +168,7 @@ export function printStatus(report) {
166
168
  console.log(`⚙️ Inference: ${inferenceStatus} (${report.inference.outcomesCount} outcomes, ${report.inference.patternsCount} patterns)`);
167
169
  console.log(`📊 Health: ${healthStatus}`);
168
170
  console.log("");
171
+ process.stdout.write(`${formatGogglesStatus(report.goggles).join("\n")}\n`);
169
172
  if (report.skills.names.length > 0) {
170
173
  console.log("Installed Skills:");
171
174
  const cols = 4;
@@ -191,9 +194,9 @@ export function printStatus(report) {
191
194
  console.log("");
192
195
  }
193
196
  }
194
- export async function statusCommand() {
197
+ export async function statusCommand(packageRoot = process.cwd()) {
195
198
  const cwd = process.cwd();
196
- const report = getStatusReport(cwd);
199
+ const report = getStatusReport(cwd, packageRoot);
197
200
  printStatus(report);
198
201
  }
199
202
  export default statusCommand;
@@ -23,8 +23,17 @@ export const VALIDATE_PACK_PATHS = [
23
23
  "dist/integrations/cursor/hooks/pre-compact.sh",
24
24
  "dist/integrations/cursor/hooks/after-file-edit.sh",
25
25
  "dist/integrations/hooks/plates.cjs",
26
+ "dist/integrations/hooks/goggles-pipeline.mjs",
27
+ "dist/integrations/hooks/goggles-planes.json",
28
+ "dist/integrations/hooks/goggles-mcp.mjs",
29
+ "scripts/mjs/run-goggles-mcp.mjs",
26
30
  "docs-site/docs/plates/memory-recall.md",
27
31
  "docs-site/docs/plates/processor.md",
32
+ "docs-site/docs/plates/goggles.md",
33
+ "docs-site/docs/plates/suit.md",
34
+ "docs-site/docs/plates/kits.md",
35
+ "docs-site/docs/plates/host-pack.md",
36
+ "docs-site/docs/plates/glossary.md",
28
37
  ];
29
38
  export const CONSUMER_WEAR_PATHS = [
30
39
  "AGENTS.md",
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Goggles on status and health.
3
+ * Organ present is not Kind 0. Quiet is a match. No phone-home.
4
+ */
5
+ export type GogglesStatus = {
6
+ organPresent: boolean;
7
+ kind0Text: string;
8
+ kind0Quiet: boolean;
9
+ kind0Label: string;
10
+ organLine: string;
11
+ };
12
+ export declare function probeGoggles(packageRoot: string, cwd: string): GogglesStatus;
13
+ export declare function formatGogglesStatus(probe: GogglesStatus): string[];
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Goggles on status and health.
3
+ * Organ present is not Kind 0. Quiet is a match. No phone-home.
4
+ */
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ const WORN_PLANES = ["dichotomy", "syncopate", "synthesis", "digest", "triage", "loop"];
8
+ const ORGAN_PAIRS = [
9
+ ["dist/integrations/hooks/goggles-pipeline.mjs", "dist/integrations/hooks/goggles-planes.json"],
10
+ ["src/integrations/hooks/goggles-pipeline.mjs", "src/integrations/hooks/goggles-planes.json"],
11
+ ];
12
+ function organPair(packageRoot) {
13
+ for (const pair of ORGAN_PAIRS) {
14
+ if (existsSync(join(packageRoot, pair[0])) && existsSync(join(packageRoot, pair[1]))) {
15
+ return pair;
16
+ }
17
+ }
18
+ return null;
19
+ }
20
+ function lensText(cwd) {
21
+ const file = join(cwd, ".xray", "state", "LENS.md");
22
+ if (!existsSync(file))
23
+ return { found: false, text: "" };
24
+ try {
25
+ return { found: true, text: readFileSync(file, "utf8").trim() };
26
+ }
27
+ catch {
28
+ return { found: false, text: "" };
29
+ }
30
+ }
31
+ function mapDrift(packageRoot, planesRel) {
32
+ try {
33
+ const data = JSON.parse(readFileSync(join(packageRoot, planesRel), "utf8"));
34
+ const map = Array.isArray(data.planes) ? data.planes.map((name) => String(name)) : [];
35
+ const same = map.length === WORN_PLANES.length && map.every((name, index) => name === WORN_PLANES[index]);
36
+ return same ? "" : "Actuality. Drift: worn is not the map.";
37
+ }
38
+ catch {
39
+ return "";
40
+ }
41
+ }
42
+ export function probeGoggles(packageRoot, cwd) {
43
+ const pair = organPair(packageRoot);
44
+ const organPresent = pair !== null;
45
+ const lens = lensText(cwd);
46
+ let kind0Text = "";
47
+ let kind0Label = "(no LENS yet)";
48
+ let kind0Quiet = false;
49
+ if (lens.found) {
50
+ kind0Text = lens.text;
51
+ if (!lens.text) {
52
+ kind0Quiet = true;
53
+ kind0Label = "quiet (match)";
54
+ }
55
+ else {
56
+ kind0Label = lens.text;
57
+ }
58
+ }
59
+ else if (pair) {
60
+ const drift = mapDrift(packageRoot, pair[1]);
61
+ if (drift) {
62
+ kind0Text = drift;
63
+ kind0Label = drift;
64
+ }
65
+ }
66
+ return {
67
+ organPresent,
68
+ kind0Text,
69
+ kind0Quiet,
70
+ kind0Label,
71
+ organLine: organPresent ? "Goggles: worn" : "Goggles: not found",
72
+ };
73
+ }
74
+ export function formatGogglesStatus(probe) {
75
+ return [probe.organLine, `Kind 0: ${probe.kind0Label}`];
76
+ }
package/dist/cli/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * Provides commands for installing and managing xray framework
6
6
  */
7
7
  import { Command } from "commander";
8
- import { execSync, spawn } from "child_process";
8
+ import { execSync, spawn, spawnSync } from "child_process";
9
9
  import { join, resolve } from "path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { readFileSync, existsSync } from "fs";
@@ -76,6 +76,43 @@ program
76
76
  process.exit(1);
77
77
  }
78
78
  });
79
+ const LOOK_HELP = "Kind 0 is quiet on a match. One outer is a reading. digest and triage return a card.";
80
+ function gogglesScript() {
81
+ const built = join(packageRoot, "dist", "integrations", "hooks", "goggles-pipeline.mjs");
82
+ const source = join(packageRoot, "src", "integrations", "hooks", "goggles-pipeline.mjs");
83
+ return existsSync(built) ? built : source;
84
+ }
85
+ function runGoggles(args) {
86
+ const script = gogglesScript();
87
+ if (!existsSync(script)) {
88
+ process.stderr.write("Goggles: not found\n");
89
+ process.exit(1);
90
+ }
91
+ if (process.stdout.isTTY) {
92
+ const result = spawnSync(process.execPath, [script, ...(args ?? [])], { stdio: "inherit" });
93
+ process.exit(result.status ?? 1);
94
+ }
95
+ const result = spawnSync(process.execPath, [script, ...(args ?? [])], { encoding: "utf8" });
96
+ if (result.stdout)
97
+ process.stdout.write(result.stdout);
98
+ if (result.stderr)
99
+ process.stderr.write(result.stderr);
100
+ process.exit(result.status ?? 1);
101
+ }
102
+ program
103
+ .command("look")
104
+ .argument("[args...]", "kind 0, one outer, or digest/triage and one card plane")
105
+ .description(LOOK_HELP)
106
+ .action((args) => {
107
+ runGoggles(args);
108
+ });
109
+ program
110
+ .command("goggles")
111
+ .argument("[args...]", "same words as look")
112
+ .description(`Same organ as look. ${LOOK_HELP}`)
113
+ .action((args) => {
114
+ runGoggles(args);
115
+ });
79
116
  program
80
117
  .command("wear")
81
118
  .description("Wear the 0xray suit in this git checkout")
@@ -115,7 +152,7 @@ program
115
152
  .description("Show comprehensive xray framework status")
116
153
  .action(async () => {
117
154
  const { statusCommand } = await import("./commands/status.js");
118
- await statusCommand();
155
+ await statusCommand(packageRoot);
119
156
  });
120
157
  program
121
158
  .command("validate")
@@ -252,6 +289,8 @@ program
252
289
  }
253
290
  }
254
291
  console.log("");
292
+ const { probeGoggles, formatGogglesStatus } = await import("./goggles-status.js");
293
+ process.stdout.write(`${formatGogglesStatus(probeGoggles(packageRoot, process.cwd())).join("\n")}\n\n`);
255
294
  if (allHealthy) {
256
295
  console.log("🎉 Framework is healthy and ready to use!");
257
296
  console.log("");
@@ -24,8 +24,11 @@ export interface GrokInstallOptions {
24
24
  export declare function installForGrokCLI(options?: GrokInstallOptions): Promise<void>;
25
25
  /** Grok TUI reads <project>/.grok/config.toml, not the package copy. */
26
26
  export declare function writeProjectRepertoireMcp(projectRoot: string): string | null;
27
+ /** Grok TUI reads <project>/.grok/config.toml. The launcher fills GOGGLES_ROOT from cwd. */
28
+ export declare function writeProjectGogglesMcp(projectRoot: string): string | null;
27
29
  declare const _default: {
28
30
  installForGrokCLI: typeof installForGrokCLI;
29
31
  writeProjectRepertoireMcp: typeof writeProjectRepertoireMcp;
32
+ writeProjectGogglesMcp: typeof writeProjectGogglesMcp;
30
33
  };
31
34
  export default _default;
@@ -27,7 +27,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
27
  const packageRoot = path.resolve(__dirname, '..', '..', '..');
28
28
  const requireCjs = createRequire(import.meta.url);
29
29
  const { resolveConsumerTargetDir, patchGrokHooks } = requireCjs(path.join(packageRoot, 'scripts/node/install-bridges.cjs'));
30
- const { XRAY_MCP_SERVERS, resolveRepertoireMcp } = requireCjs(path.join(packageRoot, 'scripts/node/bridge-mcp-wiring.cjs'));
30
+ const { XRAY_MCP_SERVERS, resolveRepertoireMcp, resolveGogglesMcp } = requireCjs(path.join(packageRoot, 'scripts/node/bridge-mcp-wiring.cjs'));
31
31
  function registerGrokMcpServers(targetDir) {
32
32
  try {
33
33
  execSync('which grok', { stdio: 'ignore' });
@@ -113,6 +113,7 @@ export async function installForGrokCLI(options = {}) {
113
113
  }
114
114
  }
115
115
  writeProjectRepertoireMcp(targetDir);
116
+ writeProjectGogglesMcp(targetDir);
116
117
  mintAfterWear(targetDir);
117
118
  const primary = dests[0];
118
119
  try {
@@ -177,6 +178,7 @@ function pinGrokPluginToInstalledDist(pluginDir, xrayRoot, targetDir) {
177
178
  }
178
179
  }
179
180
  const repertoireMcp = resolveRepertoireMcp(targetDir);
181
+ const gogglesMcp = resolveGogglesMcp(targetDir);
180
182
  if (mcp.mcpServers) {
181
183
  if (repertoireMcp) {
182
184
  mcp.mcpServers.repertoire = {
@@ -187,6 +189,16 @@ function pinGrokPluginToInstalledDist(pluginDir, xrayRoot, targetDir) {
187
189
  else {
188
190
  delete mcp.mcpServers.repertoire;
189
191
  }
192
+ if (gogglesMcp) {
193
+ mcp.mcpServers.goggles = {
194
+ command: 'node',
195
+ args: [gogglesMcp],
196
+ env: { GOGGLES_ROOT: targetDir, GOGGLES_PLANES_PATH: '' },
197
+ };
198
+ }
199
+ else {
200
+ delete mcp.mcpServers.goggles;
201
+ }
190
202
  }
191
203
  fs.writeFileSync(mcpPath, `${JSON.stringify(mcp, null, 2)}\n`);
192
204
  }
@@ -217,7 +229,35 @@ enabled = true
217
229
  }
218
230
  return tomlPath;
219
231
  }
232
+ /** Grok TUI reads <project>/.grok/config.toml. The launcher fills GOGGLES_ROOT from cwd. */
233
+ export function writeProjectGogglesMcp(projectRoot) {
234
+ const gogglesMcp = resolveGogglesMcp(projectRoot);
235
+ if (!gogglesMcp)
236
+ return null;
237
+ const grokDir = path.join(projectRoot, '.grok');
238
+ fs.mkdirSync(grokDir, { recursive: true });
239
+ const tomlPath = path.join(grokDir, 'config.toml');
240
+ const block = `[mcp_servers.goggles]
241
+ command = "node"
242
+ args = [${JSON.stringify(gogglesMcp)}]
243
+ enabled = true
244
+ `;
245
+ let existing = '';
246
+ if (fs.existsSync(tomlPath)) {
247
+ existing = fs.readFileSync(tomlPath, 'utf8');
248
+ }
249
+ if (/\[mcp_servers\.goggles\]/.test(existing)) {
250
+ existing = existing.replace(/\[mcp_servers\.goggles\][\s\S]*?(?=\n\[|$)/, block.trim());
251
+ fs.writeFileSync(tomlPath, existing.endsWith('\n') ? existing : `${existing}\n`);
252
+ }
253
+ else {
254
+ const prefix = existing.trim() ? `${existing.trim()}\n\n` : '';
255
+ fs.writeFileSync(tomlPath, `${prefix}${block}`);
256
+ }
257
+ return tomlPath;
258
+ }
220
259
  export default {
221
260
  installForGrokCLI,
222
261
  writeProjectRepertoireMcp,
262
+ writeProjectGogglesMcp,
223
263
  };
@@ -28,6 +28,7 @@ import {
28
28
  retainCompactFields,
29
29
  writeStationMarkdown,
30
30
  } from '../../hooks/station-hook-runtime.mjs';
31
+ import { handCard } from '../../hooks/goggles-pipeline.mjs';
31
32
 
32
33
  export {
33
34
  checkPendingDelegationGate,
@@ -86,12 +87,18 @@ const WRITE_TOOLS = new Set([
86
87
  'EditNotebook',
87
88
  ]);
88
89
 
90
+ export function normalizeWorkspaceRoot(root) {
91
+ const raw = String(root || '').trim();
92
+ if (!raw) return raw;
93
+ return path.resolve(raw);
94
+ }
95
+
89
96
  export function workspaceRoot() {
90
- return (
97
+ return normalizeWorkspaceRoot(
91
98
  process.env.GROK_WORKSPACE_ROOT ||
92
99
  process.env.CLAUDE_PROJECT_DIR ||
93
100
  process.env.XRAY_ROOT ||
94
- process.cwd()
101
+ process.cwd(),
95
102
  );
96
103
  }
97
104
 
@@ -198,6 +205,7 @@ export function extractToolContext(event) {
198
205
 
199
206
  if (toolInput.path) paths.push(String(toolInput.path));
200
207
  if (toolInput.file_path) paths.push(String(toolInput.file_path));
208
+ if (toolInput.target_file) paths.push(String(toolInput.target_file));
201
209
  if (toolInput.target_notebook) paths.push(String(toolInput.target_notebook));
202
210
  if (Array.isArray(toolInput.paths)) paths.push(...toolInput.paths.map(String));
203
211
 
@@ -301,7 +309,7 @@ export function buildSessionBootPayload(root, source = '0xray/grok-session-start
301
309
  ? 'xray-orchestrator analyze-complexity optional on frontier (spawn warns, does not deny)'
302
310
  : 'xray-orchestrator → analyze-complexity (required before spawn_subagent)',
303
311
  enforcement: 'PreToolUse hook — Codex constitution always on; ceremony scales by suit_temperament',
304
- workspaceRoot: root,
312
+ workspaceRoot: normalizeWorkspaceRoot(root),
305
313
  ...heat,
306
314
  ...(siblingRoots.length > 0 ? { siblingWorkspaceRoots: siblingRoots } : {}),
307
315
  ...(conferPending ? { conferPending: true, conferTrigger: 'analyze-complexity at synthesis checkpoint' } : {}),
@@ -322,6 +330,11 @@ export function writeSessionBoot(root, payload) {
322
330
  fs.mkdirSync(stateDir, { recursive: true });
323
331
  fs.writeFileSync(sessionBootPath(root), JSON.stringify(payload, null, 2));
324
332
  writeStationMarkdown(root, payload);
333
+ try {
334
+ handCard(root, payload && (payload.cardText || payload.intent));
335
+ } catch {
336
+ /* a missed card must not fail the boot */
337
+ }
325
338
  return sessionBootPath(root);
326
339
  } catch {
327
340
  return null;
@@ -333,9 +346,16 @@ export function sessionBootNeedsRefresh(existing, root) {
333
346
  if (existing.lead_dev_mode === undefined) return true;
334
347
  if (existing.host !== 'grok') return true;
335
348
  if (!existing.suit_profile) return true;
336
- if (existing.workspaceRoot && existing.workspaceRoot !== root) return true;
349
+ if (
350
+ existing.workspaceRoot &&
351
+ normalizeWorkspaceRoot(existing.workspaceRoot) !== normalizeWorkspaceRoot(root)
352
+ ) {
353
+ return true;
354
+ }
337
355
  if (!existing.repertoireResume) return true;
338
356
  if (!existing.stationLine) return true;
357
+ const liveResume = buildRepertoireResume(root);
358
+ if (liveResume && existing.repertoireResume !== liveResume) return true;
339
359
  return false;
340
360
  }
341
361
 
@@ -10,6 +10,7 @@ import {
10
10
  isSubagentTool,
11
11
  isWriteTool,
12
12
  loadFeatures,
13
+ normalizeWorkspaceRoot,
13
14
  readStdinJson,
14
15
  resolveSessionId,
15
16
  workspaceRoot,
@@ -33,7 +34,7 @@ function extractToolOutput(event) {
33
34
  }
34
35
 
35
36
  export function handlePostToolUse(event, root = workspaceRoot()) {
36
- const eventRoot = event.workspaceRoot || event.cwd || root;
37
+ const eventRoot = normalizeWorkspaceRoot(event.workspaceRoot || event.cwd || root);
37
38
  const sessionId = resolveSessionId(event);
38
39
  const { toolName, toolInput } = extractToolContext(event);
39
40
 
@@ -15,11 +15,72 @@ import {
15
15
  isShellTool,
16
16
  isWriteTool,
17
17
  loadFeatures,
18
+ normalizeWorkspaceRoot,
18
19
  readStdinJson,
19
20
  resolveSessionId,
20
21
  workspaceRoot,
21
22
  } from './grok-hook-utils.js';
22
23
  import { appendHookActivity } from './grok-hook-activity.js';
24
+ import { cardStop } from '../../hooks/goggles-pipeline.mjs';
25
+ import { createRequire } from 'node:module';
26
+ import { existsSync } from 'node:fs';
27
+ import { dirname, join } from 'node:path';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ const hookDir = dirname(fileURLToPath(import.meta.url));
31
+ let scoreAndRouteFn;
32
+
33
+ function loadScoreAndRoute() {
34
+ if (scoreAndRouteFn !== undefined) return scoreAndRouteFn;
35
+ scoreAndRouteFn = null;
36
+ const require = createRequire(import.meta.url);
37
+ const candidates = [
38
+ join(hookDir, '../../../nucleus/thin-dispatch.js'),
39
+ join(hookDir, '../../../../dist/nucleus/thin-dispatch.js'),
40
+ ];
41
+ for (const candidate of candidates) {
42
+ if (!existsSync(candidate)) continue;
43
+ try {
44
+ const loaded = require(candidate);
45
+ if (typeof loaded.scoreAndRoute === 'function') {
46
+ scoreAndRouteFn = loaded.scoreAndRoute;
47
+ return scoreAndRouteFn;
48
+ }
49
+ } catch {
50
+ /* next built copy */
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+
56
+ /** Speak only when the organ changes the agent. A match that leaves the agent is quiet. */
57
+ export function repertoireDecisionFields(routed) {
58
+ if (!routed?.memoryRouting?.overridden || !routed.agent) return null;
59
+ const signals = Array.isArray(routed.memoryRouting.signals)
60
+ ? routed.memoryRouting.signals.slice(0, 4).filter(Boolean)
61
+ : [];
62
+ const why = signals.length ? ` on ${signals.join(', ')}` : '';
63
+ return {
64
+ agent: routed.agent,
65
+ gate: 'repertoire',
66
+ hookSpecificOutput: {
67
+ hookEventName: 'PreToolUse',
68
+ additionalContext: `Repertoire changed the route to ${routed.agent}${why}. Follow that agent for this work.`,
69
+ },
70
+ };
71
+ }
72
+
73
+ function repertoireRoute(operation) {
74
+ const text = String(operation || '').slice(0, 500).trim();
75
+ if (!text) return null;
76
+ try {
77
+ const scoreAndRoute = loadScoreAndRoute();
78
+ if (!scoreAndRoute) return null;
79
+ return repertoireDecisionFields(scoreAndRoute(text, {}));
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
23
84
 
24
85
  function finish(root, decision, reason, hint, toolName, extra = {}) {
25
86
  const out = { decision, ...extra };
@@ -30,6 +91,7 @@ function finish(root, decision, reason, hint, toolName, extra = {}) {
30
91
  tool: toolName,
31
92
  reason: reason || hint || null,
32
93
  gate: extra.gate || null,
94
+ agent: extra.agent || null,
33
95
  livePath: true,
34
96
  });
35
97
  process.exit(0);
@@ -41,7 +103,7 @@ async function main() {
41
103
 
42
104
  try {
43
105
  const event = await readStdinJson();
44
- const eventRoot = event.workspaceRoot || event.cwd || root;
106
+ const eventRoot = normalizeWorkspaceRoot(event.workspaceRoot || event.cwd || root);
45
107
  ensureSessionBoot(eventRoot, '0xray/grok-pre-tool-use-boot');
46
108
 
47
109
  const features = loadFeatures(eventRoot);
@@ -75,9 +137,28 @@ async function main() {
75
137
  if (extraBlock) finish(eventRoot, 'deny', extraBlock, null, toolName);
76
138
  }
77
139
 
140
+ const stop = cardStop(
141
+ eventRoot,
142
+ toolName,
143
+ [cmd, content, toolInput.pattern, toolInput.query, ...(ctx.paths || [])].filter(Boolean).join('\n'),
144
+ ctx.paths || [],
145
+ toolInput,
146
+ );
147
+ if (stop?.decision === 'deny') {
148
+ finish(eventRoot, 'deny', stop.reason, null, toolName, { gate: stop.gate });
149
+ }
150
+ if (stop?.decision === 'allow') {
151
+ finish(eventRoot, 'allow', null, null, toolName, {
152
+ gate: stop.gate,
153
+ hookSpecificOutput: stop.hookSpecificOutput,
154
+ });
155
+ }
156
+
157
+ const route = repertoireRoute([toolName, cmd, content].filter(Boolean).join(' '));
158
+
78
159
  if (isShellTool(toolName) && cmd) {
79
160
  const testHint = checkFullTestSuite(cmd, features);
80
- if (testHint) finish(eventRoot, 'allow', null, testHint, toolName);
161
+ if (testHint) finish(eventRoot, 'allow', null, testHint, toolName, route || {});
81
162
  }
82
163
 
83
164
  if (gateBlock.reason) {
@@ -87,11 +168,11 @@ async function main() {
87
168
  gateBlock.reason,
88
169
  gateBlock.hint,
89
170
  toolName,
90
- { gate: gateBlock.gate, warn: true },
171
+ { gate: gateBlock.gate, warn: true, ...(route || {}) },
91
172
  );
92
173
  }
93
174
 
94
- finish(eventRoot, 'allow', null, null, toolName);
175
+ finish(eventRoot, 'allow', null, null, toolName, route || {});
95
176
  } catch (err) {
96
177
  appendHookActivity(root, 'grok-pre-tool-use', 'hook-error', 'error', {
97
178
  tool: toolName,
@@ -116,6 +197,7 @@ function extractFromEvent(event) {
116
197
 
117
198
  if (toolInput.path) paths.push(String(toolInput.path));
118
199
  if (toolInput.file_path) paths.push(String(toolInput.file_path));
200
+ if (toolInput.target_file) paths.push(String(toolInput.target_file));
119
201
  if (toolInput.target_notebook) paths.push(String(toolInput.target_notebook));
120
202
  if (Array.isArray(toolInput.paths)) paths.push(...toolInput.paths.map(String));
121
203
 
@@ -133,4 +215,7 @@ function extractFromEvent(event) {
133
215
  };
134
216
  }
135
217
 
136
- main();
218
+ const launchedAsCli = Boolean(process.argv[1] && process.argv[1].endsWith('pre-tool-use.js'));
219
+ if (launchedAsCli) {
220
+ main();
221
+ }