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.
- package/.grok-plugin/plugin.json +1 -1
- package/.mcp.json +8 -0
- package/.opencode/agents/api-design.yml +31 -0
- package/.opencode/agents/architect.yml +15 -0
- package/.opencode/agents/architecture-patterns.yml +14 -0
- package/.opencode/agents/auto-format.yml +14 -0
- package/.opencode/agents/backend-engineer.yml +12 -0
- package/.opencode/agents/boot-orchestrator.yml +14 -0
- package/.opencode/agents/bug-triage-specialist.yml +15 -0
- package/.opencode/agents/code-analyzer.yml +12 -0
- package/.opencode/agents/code-reviewer.yml +14 -0
- package/.opencode/agents/content-creator.yml +12 -0
- package/.opencode/agents/database-engineer.yml +12 -0
- package/.opencode/agents/devops-engineer.yml +12 -0
- package/.opencode/agents/framework-compliance-audit.yml +14 -0
- package/.opencode/agents/frontend-engineer.yml +12 -0
- package/.opencode/agents/frontend-ui-ux-engineer.yml +12 -0
- package/.opencode/agents/git-workflow.yml +15 -0
- package/.opencode/agents/growth-strategist.yml +12 -0
- package/.opencode/agents/hermes-agent.yml +12 -0
- package/.opencode/agents/inference-improve.yml +12 -0
- package/.opencode/agents/lint.yml +14 -0
- package/.opencode/agents/log-monitor.yml +12 -0
- package/.opencode/agents/mobile-developer.yml +12 -0
- package/.opencode/agents/model-health-check.yml +12 -0
- package/.opencode/agents/multimodal-looker.yml +12 -0
- package/.opencode/agents/performance-analysis.yml +12 -0
- package/.opencode/agents/performance-engineer.yml +12 -0
- package/.opencode/agents/performance-optimization.yml +12 -0
- package/.opencode/agents/processor-pipeline.yml +14 -0
- package/.opencode/agents/project-analysis.yml +12 -0
- package/.opencode/agents/refactorer.yml +12 -0
- package/.opencode/agents/researcher.yml +12 -0
- package/.opencode/agents/security-auditor.yml +12 -0
- package/.opencode/agents/security-scan.yml +12 -0
- package/.opencode/agents/seo-consultant.yml +12 -0
- package/.opencode/agents/session-management.yml +12 -0
- package/.opencode/agents/state-manager.yml +12 -0
- package/.opencode/agents/storyteller.yml +12 -0
- package/.opencode/agents/strategist.yml +12 -0
- package/.opencode/agents/tech-writer.yml +12 -0
- package/.opencode/agents/testing-best-practices.yml +12 -0
- package/.opencode/agents/testing-lead.yml +12 -0
- package/.opencode/agents/ui-ux-design.yml +12 -0
- package/.opencode/codex.codex +8 -0
- package/.opencode/commands/auto-format.md +99 -0
- package/.opencode/commands/auto-summary-capture.md +90 -0
- package/.opencode/commands/dependency-audit.md +184 -0
- package/.opencode/commands/enforcer-daily-scan.md +137 -0
- package/.opencode/commands/framework-compliance-audit.md +205 -0
- package/.opencode/commands/interactive-validator.md +75 -0
- package/.opencode/commands/job-summary-logger.md +68 -0
- package/.opencode/commands/lint.md +11 -0
- package/.opencode/commands/mode-switch.md +95 -0
- package/.opencode/commands/model-health-check.md +186 -0
- package/.opencode/commands/performance-analysis.md +144 -0
- package/.opencode/commands/pre-commit-introspection.md +185 -0
- package/.opencode/commands/pre-commit-introspection.sh +133 -0
- package/.opencode/commands/security-scan.md +157 -0
- package/.opencode/commands/sisyphus-validation.md +128 -0
- package/.opencode/commands/summary-logger.md +83 -0
- package/.opencode/enforcer-config.json +282 -0
- package/.opencode/skills/orchestrator/SKILL.md +5 -5
- package/.opencode/workflows/post-deployment-audit.yml +123 -0
- package/.xray/features.json +1 -1
- package/dist/CHANGELOG.md +16 -0
- package/dist/cli/commands/status.d.ts +4 -2
- package/dist/cli/commands/status.js +6 -3
- package/dist/cli/commands/validate.js +4 -0
- package/dist/cli/goggles-status.d.ts +13 -0
- package/dist/cli/goggles-status.js +76 -0
- package/dist/cli/index.js +31 -7
- package/dist/governance/governance-service.js +11 -1
- package/dist/integrations/grok/grok-cli.d.ts +3 -0
- package/dist/integrations/grok/grok-cli.js +41 -1
- package/dist/integrations/grok/hooks/grok-hook-utils.js +7 -4
- package/dist/integrations/grok/hooks/pre-tool-use.js +7 -1
- package/dist/integrations/grok/hooks/session-start.js +5 -2
- package/dist/integrations/hooks/goggles-mcp.mjs +338 -0
- package/dist/integrations/hooks/goggles-pipeline.d.mts +2 -0
- package/dist/integrations/hooks/goggles-pipeline.mjs +485 -100
- package/dist/integrations/hooks/plates.cjs +15 -1
- package/dist/integrations/hooks/station-hook-runtime.cjs +53 -5
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/mcps/orchestrator/config/agent-capabilities.js +5 -0
- package/dist/nucleus/thin-dispatch.d.ts +4 -0
- package/dist/nucleus/thin-dispatch.js +9 -1
- package/dist/skills/orchestrator/SKILL.md +5 -5
- package/package.json +2 -1
- package/scripts/foundry/assert-packed-dist-cli.mjs +4 -0
- package/scripts/foundry/mint-suit.cjs +6 -1
- package/scripts/mjs/run-goggles-mcp.mjs +41 -0
- package/scripts/node/bridge-mcp-wiring.cjs +61 -21
- package/scripts/node/install-bridges.cjs +24 -7
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/src/mcps/orchestrator/config/agent-capabilities.ts +4 -0
- package/src/skills/orchestrator/SKILL.md +5 -5
- 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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
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 (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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 =
|
|
1829
|
-
if (resolved ===
|
|
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 =
|
|
1841
|
-
if (resolved ===
|
|
1842
|
-
if (roots.some((item) =>
|
|
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 (
|
|
2066
|
+
if (realPath(top) !== realPath(targetDir)) {
|
|
2050
2067
|
throw new Error("setup: work tree points elsewhere");
|
|
2051
2068
|
}
|
|
2052
2069
|
}
|
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|