strray-ai 1.22.44 → 1.22.45

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.
@@ -1,21 +1,30 @@
1
1
  /**
2
- * StrRay Codex Injection Plugin for OpenCode
2
+ * 0xRay Codex Injection Plugin for OpenCode
3
3
  *
4
4
  * This plugin automatically injects the Universal Development Codex v1.2.0
5
5
  * into the system prompt for all AI agents, ensuring codex terms are
6
6
  * consistently enforced across the entire development session.
7
7
  *
8
8
  * @version 1.0.0
9
- * @author StrRay Framework
9
+ * @author 0xRay Framework
10
10
  */
11
11
  import * as fs from "fs";
12
12
  import * as path from "path";
13
13
  import { spawn } from "child_process";
14
- // Dynamic imports for config-paths and framework-logger
15
- // Uses candidate-based resolution to work from both dist/plugin/ and .opencode/plugin/
16
- let _resolveCodexPath;
17
- let _resolveStateDir;
18
- let _frameworkLogger;
14
+ // ---------------------------------------------------------------------------
15
+ // Dynamic module holders (loaded via candidate-based resolution)
16
+ // ---------------------------------------------------------------------------
17
+ let _resolveCodexPath = null;
18
+ let _resolveStateDir = null;
19
+ let _frameworkLogger = null;
20
+ let _systemPromptGenerator = null;
21
+ let _ProcessorManager = null;
22
+ let _StrRayStateManager = null;
23
+ let _featuresConfigLoader = null;
24
+ let _detectTaskType = null;
25
+ // ---------------------------------------------------------------------------
26
+ // Module loaders (candidate-based resolution for dev, dist, and consumer paths)
27
+ // ---------------------------------------------------------------------------
19
28
  async function loadFrameworkLogger() {
20
29
  if (_frameworkLogger)
21
30
  return _frameworkLogger;
@@ -34,7 +43,6 @@ async function loadFrameworkLogger() {
34
43
  // try next candidate
35
44
  }
36
45
  }
37
- // Fallback: no-op logger so plugin doesn't crash
38
46
  _frameworkLogger = {
39
47
  log: (_module, _event, _status, _data) => { },
40
48
  };
@@ -62,63 +70,59 @@ async function loadConfigPaths() {
62
70
  const logger = await loadFrameworkLogger();
63
71
  logger.log("strray-codex-plugin", "config-paths-load-failed", "warning", { warning: "Failed to load config-paths module from any location" });
64
72
  }
65
- /** Convenience wrapper — must be awaited before use */
66
- async function resolveCodexPath(...args) {
73
+ async function resolveCodexPath(root) {
67
74
  await loadConfigPaths();
68
- return _resolveCodexPath(...args);
75
+ if (!_resolveCodexPath)
76
+ throw new Error("resolveCodexPath not available after loading");
77
+ return _resolveCodexPath(root);
69
78
  }
70
- async function resolveStateDir(...args) {
79
+ async function resolveStateDir(root) {
71
80
  await loadConfigPaths();
72
- return _resolveStateDir(...args);
81
+ if (!_resolveStateDir)
82
+ throw new Error("resolveStateDir not available after loading");
83
+ return _resolveStateDir(root);
73
84
  }
74
- // Import lean system prompt generator
75
- let SystemPromptGenerator;
76
85
  async function importSystemPromptGenerator() {
77
- if (!SystemPromptGenerator) {
78
- const candidates = [
79
- "../core/system-prompt-generator.js",
80
- "../../dist/core/system-prompt-generator.js",
81
- "../../node_modules/strray-ai/dist/core/system-prompt-generator.js",
82
- ];
83
- for (const p of candidates) {
84
- try {
85
- const module = await import(p);
86
- SystemPromptGenerator = module.generateLeanSystemPrompt;
87
- return;
88
- }
89
- catch (_) {
90
- // try next candidate
91
- }
86
+ if (_systemPromptGenerator)
87
+ return;
88
+ const candidates = [
89
+ "../core/system-prompt-generator.js",
90
+ "../../dist/core/system-prompt-generator.js",
91
+ "../../node_modules/strray-ai/dist/core/system-prompt-generator.js",
92
+ ];
93
+ for (const p of candidates) {
94
+ try {
95
+ const module = await import(p);
96
+ _systemPromptGenerator = module.generateLeanSystemPrompt;
97
+ return;
98
+ }
99
+ catch (_) {
100
+ // try next candidate
92
101
  }
93
- const logger = await loadFrameworkLogger();
94
- logger.log("strray-codex-plugin", "system-prompt-generator-load-failed", "warning", { warning: "Failed to load lean system prompt generator, using fallback" });
95
102
  }
103
+ const logger = await loadFrameworkLogger();
104
+ logger.log("strray-codex-plugin", "system-prompt-generator-load-failed", "warning", { warning: "Failed to load lean system prompt generator, using fallback" });
96
105
  }
97
- let ProcessorManager;
98
- let StrRayStateManager;
99
- let featuresConfigLoader;
100
- let detectTaskType;
101
- async function loadStrRayComponents() {
102
- if (ProcessorManager && StrRayStateManager && featuresConfigLoader)
106
+ async function loadStringRayComponents() {
107
+ if (_ProcessorManager && _StrRayStateManager && _featuresConfigLoader)
103
108
  return;
104
109
  const logger = await getOrCreateLogger(process.cwd());
105
- // Try local dist first (for development)
106
110
  try {
107
111
  logger.log(`🔄 Attempting to load from ../../dist/`);
108
112
  const procModule = await import("../../dist/processors/processor-manager.js");
109
113
  const stateModule = await import("../../dist/state/state-manager.js");
110
114
  const featuresModule = await import("../../dist/core/features-config.js");
111
- ProcessorManager = procModule.ProcessorManager;
112
- StrRayStateManager = stateModule.StrRayStateManager;
113
- featuresConfigLoader = featuresModule.featuresConfigLoader;
114
- detectTaskType = featuresModule.detectTaskType;
115
+ _ProcessorManager = procModule.ProcessorManager;
116
+ _StrRayStateManager = stateModule.StrRayStateManager;
117
+ _featuresConfigLoader = featuresModule.featuresConfigLoader;
118
+ _detectTaskType = featuresModule.detectTaskType;
115
119
  logger.log(`✅ Loaded from ../../dist/`);
116
120
  return;
117
121
  }
118
122
  catch (e) {
119
- logger.error(`❌ Failed to load from ../../dist/: ${e?.message || e}`);
123
+ const message = e instanceof Error ? e.message : String(e);
124
+ logger.log(`❌ Failed to load from ../../dist/: ${message}`);
120
125
  }
121
- // Try node_modules (for consumer installation)
122
126
  const pluginPaths = ["strray-ai", "strray-framework"];
123
127
  for (const pluginPath of pluginPaths) {
124
128
  try {
@@ -126,28 +130,29 @@ async function loadStrRayComponents() {
126
130
  const pm = await import(`../../node_modules/${pluginPath}/dist/processors/processor-manager.js`);
127
131
  const sm = await import(`../../node_modules/${pluginPath}/dist/state/state-manager.js`);
128
132
  const fm = await import(`../../node_modules/${pluginPath}/dist/core/features-config.js`);
129
- ProcessorManager = pm.ProcessorManager;
130
- StrRayStateManager = sm.StrRayStateManager;
131
- featuresConfigLoader = fm.featuresConfigLoader;
132
- detectTaskType = fm.detectTaskType;
133
+ _ProcessorManager = pm.ProcessorManager;
134
+ _StrRayStateManager = sm.StrRayStateManager;
135
+ _featuresConfigLoader = fm.featuresConfigLoader;
136
+ _detectTaskType = fm.detectTaskType;
133
137
  logger.log(`✅ Loaded from ../../node_modules/${pluginPath}/dist/`);
134
138
  return;
135
139
  }
136
140
  catch (e) {
137
- logger.error(`❌ Failed to load from ../../node_modules/${pluginPath}/dist/: ${e?.message || e}`);
138
- continue;
141
+ const message = e instanceof Error ? e.message : String(e);
142
+ logger.log(`❌ Failed to load from ../../node_modules/${pluginPath}/dist/: ${message}`);
139
143
  }
140
144
  }
141
145
  }
146
+ // ---------------------------------------------------------------------------
147
+ // Helpers
148
+ // ---------------------------------------------------------------------------
142
149
  function spawnPromise(command, args, cwd) {
143
150
  return new Promise((resolve, reject) => {
144
151
  const child = spawn(command, args, {
145
152
  cwd,
146
- stdio: ["ignore", "inherit", "pipe"], // Original working stdio - stdout to terminal (ASCII visible)
153
+ stdio: ["ignore", "inherit", "pipe"],
147
154
  });
148
- let stdout = "";
149
155
  let stderr = "";
150
- // Capture stderr only (stdout goes to inherit/terminal)
151
156
  if (child.stderr) {
152
157
  child.stderr.on("data", (data) => {
153
158
  stderr += data.toString();
@@ -155,7 +160,7 @@ function spawnPromise(command, args, cwd) {
155
160
  }
156
161
  child.on("close", (code) => {
157
162
  if (code === 0) {
158
- resolve({ stdout, stderr });
163
+ resolve({ stdout: "", stderr });
159
164
  }
160
165
  else {
161
166
  reject(new Error(`Process exited with code ${code}: ${stderr}`));
@@ -182,7 +187,7 @@ class PluginLogger {
182
187
  const logEntry = `[${timestamp}] ${message}\n`;
183
188
  await fs.promises.appendFile(this.logPath, logEntry, "utf-8");
184
189
  }
185
- catch (error) {
190
+ catch {
186
191
  // Silent fail - logging failure should not break plugin
187
192
  }
188
193
  }
@@ -210,9 +215,6 @@ async function getOrCreateLogger(directory) {
210
215
  })();
211
216
  return loggerInitPromise;
212
217
  }
213
- /**
214
- * Get the current framework version from package.json
215
- */
216
218
  function getFrameworkVersion() {
217
219
  try {
218
220
  const packageJsonPath = path.join(process.cwd(), "package.json");
@@ -223,12 +225,9 @@ function getFrameworkVersion() {
223
225
  return "1.4.6";
224
226
  }
225
227
  }
226
- /**
227
- * Get lean framework identity message (token-efficient version)
228
- */
229
228
  function getFrameworkIdentity() {
230
229
  const version = getFrameworkVersion();
231
- return `StringRay Framework v${version} - AI Orchestration
230
+ return `0xRay Framework v${version} - AI Orchestration
232
231
 
233
232
  🔧 Core: enforcer, architect, orchestrator, code-reviewer, refactorer, testing-lead
234
233
  📚 Codex: 5 Essential Terms (99.6% Error Prevention Target)
@@ -237,18 +236,15 @@ function getFrameworkIdentity() {
237
236
  📖 Documentation: config dir (codex, config, agents docs) — resolved via config-paths
238
237
  `;
239
238
  }
240
- /**
241
- * Run Enforcer quality gate check before operations
242
- * Now delegates to RuleEnforcer for proper rule enforcement
243
- */
239
+ // ---------------------------------------------------------------------------
240
+ // Enforcer quality gate
241
+ // ---------------------------------------------------------------------------
244
242
  async function runEnforcerQualityGate(input, logger) {
245
243
  const violations = [];
246
244
  const { tool, args } = input;
247
245
  try {
248
- // Lazy load RuleEnforcer to avoid circular dependencies
249
246
  const { RuleEnforcer } = await import("../enforcement/rule-enforcer.js");
250
247
  const ruleEnforcer = new RuleEnforcer();
251
- // Build validation context from the input
252
248
  const context = {
253
249
  operation: tool === "write" ? "write" : tool === "edit" ? "edit" : "read",
254
250
  };
@@ -258,25 +254,20 @@ async function runEnforcerQualityGate(input, logger) {
258
254
  if (args?.content) {
259
255
  context.newCode = args.content;
260
256
  }
261
- // Use RuleEnforcer.validateOperation() instead of hardcoded rules
262
257
  const report = await ruleEnforcer.validateOperation(tool, context);
263
- // Collect blocking violations from errors and failed results
264
258
  const blockingViolations = [];
265
259
  const allViolations = [];
266
- // Check errors (these are blocking by nature)
267
260
  if (report.errors && report.errors.length > 0) {
268
261
  for (const error of report.errors) {
269
262
  allViolations.push(error);
270
- blockingViolations.push(error); // All errors are blocking
263
+ blockingViolations.push(error);
271
264
  }
272
265
  }
273
- // Check failed results - only block on error severity, not warnings
274
266
  if (report.results) {
275
267
  for (const result of report.results) {
276
268
  if (!result.passed) {
277
269
  const isBlocking = result.severity === "error" || result.severity === "blocking" || result.severity === "high";
278
270
  allViolations.push(result.message);
279
- // Only block on error/blocking/high severity, allow warnings
280
271
  if (isBlocking) {
281
272
  blockingViolations.push(result.message);
282
273
  }
@@ -303,9 +294,7 @@ async function runEnforcerQualityGate(input, logger) {
303
294
  return { passed, violations };
304
295
  }
305
296
  catch (error) {
306
- // If RuleEnforcer fails to load or run, fall back to minimal checks
307
297
  logger.log(`Warning: RuleEnforcer unavailable, using fallback checks: ${error instanceof Error ? error.message : String(error)}`);
308
- // Fallback: minimal hardcoded checks (the old logic, as safety net)
309
298
  if (tool === "write" && args?.filePath) {
310
299
  const filePath = args.filePath;
311
300
  if (filePath.endsWith(".ts") &&
@@ -337,24 +326,13 @@ async function runEnforcerQualityGate(input, logger) {
337
326
  return { passed, violations };
338
327
  }
339
328
  }
340
- /**
341
- * Global codex context cache (loaded once)
342
- */
343
329
  let cachedCodexContexts = null;
344
- /**
345
- * Codex file locations resolved through the standard priority chain.
346
- * Falls back to additional OpenCode-specific files not covered by the resolver.
347
- */
348
330
  async function getCodexFileLocations(directory) {
349
331
  const root = directory || process.cwd();
350
332
  const resolved = await resolveCodexPath(root);
351
- // Add OpenCode-specific fallbacks not in the standard chain
352
333
  resolved.push(path.join(root, ".opencode", "codex.codex"), path.join(root, ".strray", "agents_template.md"), path.join(root, ".opencode", "strray", "agents_template.md"), path.join(root, "AGENTS.md"));
353
334
  return resolved;
354
335
  }
355
- /**
356
- * Read file content safely
357
- */
358
336
  function readFileContent(filePath) {
359
337
  try {
360
338
  return fs.readFileSync(filePath, "utf-8");
@@ -365,11 +343,7 @@ function readFileContent(filePath) {
365
343
  return null;
366
344
  }
367
345
  }
368
- /**
369
- * Extract codex metadata from content
370
- */
371
346
  function extractCodexMetadata(content) {
372
- // Try JSON format first (codex.json)
373
347
  if (content.trim().startsWith("{")) {
374
348
  try {
375
349
  const parsed = JSON.parse(content);
@@ -382,16 +356,12 @@ function extractCodexMetadata(content) {
382
356
  // Not valid JSON, try markdown format
383
357
  }
384
358
  }
385
- // Markdown format (AGENTS.md, agents_template.md via config-paths resolver)
386
359
  const versionMatch = content.match(/\*\*Version\*\*:\s*(\d+\.\d+\.\d+)/);
387
- const version = versionMatch && versionMatch[1] ? versionMatch[1] : "1.6.0";
360
+ const version = versionMatch?.[1] ?? "1.6.0";
388
361
  const termMatches = content.match(/####\s*\d+\.\s/g);
389
362
  const termCount = termMatches ? termMatches.length : 0;
390
363
  return { version, termCount };
391
364
  }
392
- /**
393
- * Create codex context entry
394
- */
395
365
  function createCodexContextEntry(filePath, content) {
396
366
  const metadata = extractCodexMetadata(content);
397
367
  return {
@@ -406,9 +376,6 @@ function createCodexContextEntry(filePath, content) {
406
376
  },
407
377
  };
408
378
  }
409
- /**
410
- * Load codex context (cached globally, loaded once)
411
- */
412
379
  async function loadCodexContext(directory) {
413
380
  if (cachedCodexContexts) {
414
381
  return cachedCodexContexts;
@@ -431,34 +398,22 @@ async function loadCodexContext(directory) {
431
398
  }
432
399
  return codexContexts;
433
400
  }
434
- /**
435
- * Format codex context for injection
436
- */
437
401
  function formatCodexContext(contexts) {
438
402
  if (contexts.length === 0) {
439
403
  return "";
440
404
  }
441
405
  const parts = [];
442
406
  for (const context of contexts) {
443
- parts.push(`# StrRay Codex Context v${context.metadata.version}`, `Source: ${context.source}`, `Terms Loaded: ${context.metadata.termCount}`, `Loaded At: ${context.metadata.loadedAt}`, "", context.content, "", "---", "");
407
+ parts.push(`# 0xRay Codex Context v${context.metadata.version}`, `Source: ${context.source}`, `Terms Loaded: ${context.metadata.termCount}`, `Loaded At: ${context.metadata.loadedAt}`, "", context.content, "", "---", "");
444
408
  }
445
409
  return parts.join("\n");
446
410
  }
447
- /**
448
- * Main plugin function
449
- *
450
- * This plugin hooks into experimental.chat.system.transform event
451
- * to inject codex terms into system prompt before it's sent to LLM.
452
- */
453
- /** Inference tuning: run every N tool calls */
411
+ // ---------------------------------------------------------------------------
412
+ // Analytics and task classification
413
+ // ---------------------------------------------------------------------------
454
414
  const INFERENCE_TUNE_INTERVAL = 100;
455
415
  let _openCodeToolCallCount = 0;
456
416
  let _lastTuneToolCallCount = 0;
457
- /**
458
- * Map tool names to agent/skill identifiers for outcome tracking.
459
- * This lets the analytics pipeline correlate tool usage patterns
460
- * with agent routing effectiveness.
461
- */
462
417
  const TOOL_AGENT_MAP = {
463
418
  write: { agent: "code-reviewer", skill: "write" },
464
419
  edit: { agent: "code-reviewer", skill: "edit" },
@@ -470,11 +425,6 @@ const TOOL_AGENT_MAP = {
470
425
  grep: { agent: "researcher", skill: "search" },
471
426
  ls: { agent: "researcher", skill: "list" },
472
427
  };
473
- /**
474
- * Classify a tool call into a meaningful task type for analytics.
475
- * Mirrors _classify_task_type in the Hermes plugin so both plugins
476
- * produce comparable outcome data for the inference tuner.
477
- */
478
428
  function classifyTaskType(tool, args) {
479
429
  const cmd = String(args?.command ?? "").toLowerCase().trim();
480
430
  if (tool === "bash" && cmd) {
@@ -503,36 +453,104 @@ function classifyTaskType(tool, args) {
503
453
  return "search";
504
454
  return "unknown";
505
455
  }
456
+ // ---------------------------------------------------------------------------
457
+ // Shared helpers extracted from duplicated plugin logic
458
+ // ---------------------------------------------------------------------------
459
+ function isWriteEditOperation(tool) {
460
+ return tool === "write" || tool === "edit" || tool === "multiedit";
461
+ }
462
+ function isPublishOperation(tool) {
463
+ return tool === "publish" || tool === "release" || tool === "npm-publish" || tool === "strray-release";
464
+ }
465
+ function resolveAgentName(input) {
466
+ const globalAgent = globalThis.currentAgent;
467
+ if (globalAgent?.agentType)
468
+ return globalAgent.agentType;
469
+ if (globalAgent?.type)
470
+ return globalAgent.type;
471
+ if (input?.agentType)
472
+ return input.agentType;
473
+ return "orchestrator";
474
+ }
475
+ function registerAllProcessors(pm) {
476
+ pm.registerProcessor({ name: "preValidate", type: "pre", priority: 10, enabled: true });
477
+ pm.registerProcessor({ name: "codexCompliance", type: "pre", priority: 20, enabled: true });
478
+ pm.registerProcessor({ name: "versionCompliance", type: "pre", priority: 25, enabled: true });
479
+ pm.registerProcessor({ name: "testAutoCreation", type: "post", priority: 5, enabled: true });
480
+ pm.registerProcessor({ name: "testExecution", type: "post", priority: 10, enabled: true });
481
+ pm.registerProcessor({ name: "coverageAnalysis", type: "post", priority: 20, enabled: true });
482
+ }
483
+ function registerAfterPostProcessors(pm) {
484
+ pm.registerProcessor({ name: "testAutoCreation", type: "post", priority: 50, enabled: true });
485
+ pm.registerProcessor({ name: "testExecution", type: "post", priority: 10, enabled: true });
486
+ pm.registerProcessor({ name: "coverageAnalysis", type: "post", priority: 20, enabled: true });
487
+ }
488
+ function logPreProcessorResults(results, logger) {
489
+ logger.log(`📊 Pre-processor result: ${results.success ? "SUCCESS" : "FAILED"} (${results.results.length} processors)`);
490
+ if (!results.success) {
491
+ const failures = results.results.filter((r) => !r.success);
492
+ for (const f of failures) {
493
+ logger.error(`❌ Pre-processor ${f.processorName} failed: ${f.error}`);
494
+ }
495
+ }
496
+ else {
497
+ for (const r of results.results) {
498
+ logger.log(`✅ Pre-processor ${r.processorName}: ${r.success ? "OK" : "FAILED"}`);
499
+ }
500
+ }
501
+ }
502
+ function logPostProcessorResults(results, logger) {
503
+ const allSuccess = results.every((r) => r.success);
504
+ logger.log(`📊 Post-processor result: ${allSuccess ? "SUCCESS" : "FAILED"} (${results.length} processors)`);
505
+ for (const r of results) {
506
+ if (r.success) {
507
+ logger.log(`✅ Post-processor ${r.processorName}: OK`);
508
+ }
509
+ else {
510
+ logger.error(`❌ Post-processor ${r.processorName} failed: ${r.error}`);
511
+ }
512
+ }
513
+ }
514
+ function logTestAutoCreationResult(results, logger) {
515
+ const testAutoResult = results.find((r) => r.processorName === "testAutoCreation");
516
+ if (testAutoResult) {
517
+ const data = testAutoResult.data;
518
+ if (testAutoResult.success && data?.testCreated && data?.testFile) {
519
+ logger.log(`✅ TEST AUTO-CREATION: Created ${data.testFile}`);
520
+ }
521
+ else if (!testAutoResult.success) {
522
+ logger.log(`â„šī¸ TEST AUTO-CREATION: ${testAutoResult.error ?? "skipped - no new files"}`);
523
+ }
524
+ }
525
+ }
526
+ // ---------------------------------------------------------------------------
527
+ // Main plugin function
528
+ // ---------------------------------------------------------------------------
506
529
  export default async function strrayCodexPlugin(input) {
507
530
  const { directory: inputDirectory } = input;
508
531
  const directory = inputDirectory || process.cwd();
509
532
  return {
510
533
  "experimental.chat.system.transform": async (_input, output) => {
511
534
  try {
512
- // Use lean system prompt generator for token efficiency
513
535
  await importSystemPromptGenerator();
514
536
  let leanPrompt = getFrameworkIdentity();
515
- // Use lean generator if available, otherwise fall back to minimal logic
516
- if (SystemPromptGenerator) {
517
- leanPrompt = await SystemPromptGenerator({
537
+ if (_systemPromptGenerator) {
538
+ leanPrompt = await _systemPromptGenerator({
518
539
  showWelcomeBanner: true,
519
- showCodexContext: false, // Disabled for token efficiency
540
+ showCodexContext: false,
520
541
  enableTokenOptimization: true,
521
- maxTokenBudget: 3000, // Conservative token budget
542
+ maxTokenBudget: 8192,
522
543
  showCriticalTermsOnly: true,
523
- showEssentialLinks: true
544
+ showEssentialLinks: true,
524
545
  });
525
546
  }
526
547
  if (output.system && Array.isArray(output.system)) {
527
- // Replace verbose system prompt with lean version
528
548
  output.system = [leanPrompt];
529
549
  }
530
550
  }
531
551
  catch (error) {
532
- // Critical failure - log error but don't break the plugin
533
552
  const logger = await getOrCreateLogger(directory);
534
553
  logger.error("System prompt injection failed:", error);
535
- // Fallback to minimal prompt
536
554
  const fallback = getFrameworkIdentity();
537
555
  if (output.system && Array.isArray(output.system)) {
538
556
  output.system = [fallback];
@@ -543,13 +561,15 @@ export default async function strrayCodexPlugin(input) {
543
561
  const logger = await getOrCreateLogger(directory);
544
562
  logger.log(`🚀 TOOL EXECUTE BEFORE HOOK FIRED: ${input.tool}`);
545
563
  logger.log(`đŸ“Ĩ Full input: ${JSON.stringify(input)}`);
546
- await loadStrRayComponents();
547
- if (featuresConfigLoader && detectTaskType) {
564
+ await loadStringRayComponents();
565
+ if (_featuresConfigLoader && _detectTaskType) {
548
566
  try {
549
- const config = featuresConfigLoader.loadConfig();
567
+ const config = _featuresConfigLoader.loadConfig();
550
568
  if (config.model_routing?.enabled) {
551
- const taskType = detectTaskType(input.tool);
552
- const routing = config.model_routing.task_routing?.[taskType];
569
+ const taskType = _detectTaskType(input.tool);
570
+ const routing = taskType !== "unknown"
571
+ ? config.model_routing.task_routing?.[taskType]
572
+ : undefined;
553
573
  if (routing?.model) {
554
574
  output.model = routing.model;
555
575
  logger.log(`Model routed: ${input.tool} → ${taskType} → ${routing.model}`);
@@ -561,122 +581,61 @@ export default async function strrayCodexPlugin(input) {
561
581
  }
562
582
  }
563
583
  const { tool, args } = input;
564
- // ENFORCER QUALITY GATE CHECK - Block on violations
565
584
  const qualityGateResult = await runEnforcerQualityGate(input, logger);
566
585
  if (!qualityGateResult.passed) {
567
586
  logger.error(`đŸšĢ Quality gate failed: ${qualityGateResult.violations.join(", ")}`);
568
587
  throw new Error(`ENFORCER BLOCKED: ${qualityGateResult.violations.join("; ")}`);
569
588
  }
570
589
  logger.log(`✅ Quality gate passed for ${tool}`);
571
- if (["write", "edit", "multiedit"].includes(tool)) {
572
- if (!ProcessorManager || !StrRayStateManager) {
590
+ if (isWriteEditOperation(tool)) {
591
+ if (!_ProcessorManager || !_StrRayStateManager) {
573
592
  logger.error("ProcessorManager or StrRayStateManager not loaded");
574
593
  return;
575
594
  }
576
- // PHASE 1: Connect to booted framework or boot if needed
577
595
  let stateManager;
578
596
  let processorManager;
579
- // Check if framework is already booted (global state exists)
580
597
  const globalState = globalThis.strRayStateManager;
581
598
  if (globalState) {
582
- logger.log("🔗 Connecting to booted StrRay framework");
599
+ logger.log("🔗 Connecting to booted 0xRay framework");
583
600
  stateManager = globalState;
584
601
  }
585
602
  else {
586
- logger.log("🚀 StrRay framework not booted, initializing...");
587
- // Create new state manager (framework not booted yet)
588
- stateManager = new StrRayStateManager(await resolveStateDir(directory));
589
- // Store globally for future use
603
+ logger.log("🚀 0xRay framework not booted, initializing...");
604
+ stateManager = new _StrRayStateManager(await resolveStateDir(directory));
590
605
  globalThis.strRayStateManager = stateManager;
591
606
  }
592
- // Get processor manager from state
593
- processorManager = stateManager.get("processor:manager");
607
+ processorManager = stateManager.get("processor:manager") ?? null;
594
608
  if (!processorManager) {
595
609
  logger.log("âš™ī¸ Creating and registering processors...");
596
- processorManager = new ProcessorManager(stateManager);
597
- // Register the same processors as boot-orchestrator
598
- processorManager.registerProcessor({
599
- name: "preValidate",
600
- type: "pre",
601
- priority: 10,
602
- enabled: true,
603
- });
604
- processorManager.registerProcessor({
605
- name: "codexCompliance",
606
- type: "pre",
607
- priority: 20,
608
- enabled: true,
609
- });
610
- processorManager.registerProcessor({
611
- name: "versionCompliance",
612
- type: "pre",
613
- priority: 25,
614
- enabled: true,
615
- });
616
- processorManager.registerProcessor({
617
- name: "testAutoCreation",
618
- type: "post",
619
- priority: 5, // FIX: Run BEFORE testExecution so tests exist when we run them
620
- enabled: true,
621
- });
622
- processorManager.registerProcessor({
623
- name: "testExecution",
624
- type: "post",
625
- priority: 10,
626
- enabled: true,
627
- });
628
- processorManager.registerProcessor({
629
- name: "coverageAnalysis",
630
- type: "post",
631
- priority: 20,
632
- enabled: true,
633
- });
634
- // Store for future use
610
+ processorManager = new _ProcessorManager(stateManager);
611
+ registerAllProcessors(processorManager);
635
612
  stateManager.set("processor:manager", processorManager);
636
613
  logger.log("✅ Processors registered successfully");
637
614
  }
638
615
  else {
639
616
  logger.log("✅ Using existing processor manager");
640
617
  }
641
- // PHASE 2: Execute pre-processors with detailed logging
642
618
  try {
643
619
  logger.log(`â–ļī¸ Executing pre-processors for ${tool}...`);
644
- const result = await processorManager.executePreProcessors({
620
+ const preProcessorInput = {
645
621
  tool,
646
- args,
622
+ args: args,
647
623
  context: {
648
624
  directory,
649
625
  operation: "tool_execution",
650
626
  filePath: args?.filePath,
651
627
  },
652
- });
653
- logger.log(`📊 Pre-processor result: ${result.success ? "SUCCESS" : "FAILED"} (${result.results?.length || 0} processors)`);
654
- if (!result.success) {
655
- const failures = result.results?.filter((r) => !r.success) || [];
656
- failures.forEach((f) => {
657
- logger.error(`❌ Pre-processor ${f.processorName} failed: ${f.error}`);
658
- });
659
- }
660
- else {
661
- result.results?.forEach((r) => {
662
- logger.log(`✅ Pre-processor ${r.processorName}: ${r.success ? "OK" : "FAILED"}`);
663
- });
664
- }
628
+ };
629
+ const result = await processorManager.executePreProcessors(preProcessorInput);
630
+ logPreProcessorResults(result, logger);
665
631
  }
666
632
  catch (error) {
667
633
  logger.error(`đŸ’Ĩ Pre-processor execution error`, error);
668
634
  }
669
- // PHASE 3: Execute post-processors after tool completion
670
635
  try {
671
636
  logger.log(`â–ļī¸ Executing post-processors for ${tool}...`);
672
637
  logger.log(`📝 Post-processor args: ${JSON.stringify(args)}`);
673
- // Determine operation type and enrich context with metadata for processors
674
- const isPublishOperation = tool === "publish" || tool === "release" || tool === "npm-publish" || tool === "strray-release";
675
- // Get agent info from session state if available
676
- const agentName = global.currentAgent?.agentType ||
677
- global.currentAgent?.type ||
678
- input.agentType ||
679
- "orchestrator";
638
+ const agentName = resolveAgentName(input);
680
639
  const postProcessorContext = {
681
640
  directory,
682
641
  operation: tool,
@@ -684,7 +643,7 @@ export default async function strrayCodexPlugin(input) {
684
643
  success: true,
685
644
  agentName,
686
645
  metadata: {
687
- isPublishing: isPublishOperation,
646
+ isPublishing: isPublishOperation(tool),
688
647
  hook: "tool_execution",
689
648
  toolName: tool,
690
649
  timestamp: Date.now(),
@@ -692,32 +651,17 @@ export default async function strrayCodexPlugin(input) {
692
651
  },
693
652
  };
694
653
  const postResults = await processorManager.executePostProcessors(tool, postProcessorContext, []);
695
- // postResults is an array of ProcessorResult
696
- const allSuccess = postResults.every((r) => r.success);
697
- logger.log(`📊 Post-processor result: ${allSuccess ? "SUCCESS" : "FAILED"} (${postResults.length} processors)`);
698
- // Log each post-processor result for debugging
699
- for (const r of postResults) {
700
- if (r.success) {
701
- logger.log(`✅ Post-processor ${r.processorName}: OK`);
702
- }
703
- else {
704
- logger.error(`❌ Post-processor ${r.processorName} failed: ${r.error}`);
705
- }
706
- }
654
+ logPostProcessorResults(postResults, logger);
707
655
  }
708
656
  catch (error) {
709
657
  logger.error(`đŸ’Ĩ Post-processor execution error`, error);
710
658
  }
711
659
  }
712
660
  },
713
- // Execute POST-processors AFTER tool completes (this is the correct place!)
714
661
  "tool.execute.after": async (input, _output) => {
715
662
  const logger = await getOrCreateLogger(directory);
716
- await loadStrRayComponents();
663
+ await loadStringRayComponents();
717
664
  const { tool, args, result } = input;
718
- // Record routing outcome for analytics pipeline.
719
- // This feeds the inference tuner with real tool usage data so it
720
- // can refine keyword mappings and improve predictive analytics.
721
665
  try {
722
666
  const { routingOutcomeTracker } = await import("../delegation/analytics/outcome-tracker.js");
723
667
  const mapping = TOOL_AGENT_MAP[tool];
@@ -746,82 +690,37 @@ export default async function strrayCodexPlugin(input) {
746
690
  catch {
747
691
  // Outcome tracker not available — skip silently
748
692
  }
749
- // Debug: log full input
750
693
  logger.log(`đŸ“Ĩ After hook input: ${JSON.stringify({ tool, hasArgs: !!args, args, hasResult: !!result }).slice(0, 200)}`);
751
- // Run post-processors for write/edit operations AFTER tool completes
752
- if (["write", "edit", "multiedit"].includes(tool)) {
753
- if (!ProcessorManager || !StrRayStateManager)
694
+ if (isWriteEditOperation(tool)) {
695
+ if (!_ProcessorManager || !_StrRayStateManager)
754
696
  return;
755
- const stateManager = new StrRayStateManager(await resolveStateDir(directory));
756
- const processorManager = new ProcessorManager(stateManager);
757
- // Register post-processors
758
- processorManager.registerProcessor({
759
- name: "testAutoCreation",
760
- type: "post",
761
- priority: 50,
762
- enabled: true,
763
- });
764
- processorManager.registerProcessor({
765
- name: "testExecution",
766
- type: "post",
767
- priority: 10,
768
- enabled: true,
769
- });
770
- processorManager.registerProcessor({
771
- name: "coverageAnalysis",
772
- type: "post",
773
- priority: 20,
774
- enabled: true,
775
- });
697
+ const stateManager = new _StrRayStateManager(await resolveStateDir(directory));
698
+ const processorManager = new _ProcessorManager(stateManager);
699
+ registerAfterPostProcessors(processorManager);
776
700
  try {
777
- // Execute post-processors AFTER tool - with actual filePath for testAutoCreation
778
701
  logger.log(`📝 Post-processor tool: ${tool}`);
779
702
  logger.log(`📝 Post-processor args: ${JSON.stringify(args)}`);
780
703
  logger.log(`📝 Post-processor directory: ${directory}`);
781
- // Determine operation type and enrich context with metadata for processors
782
- const isPublishOperation = tool === "publish" || tool === "release" || tool === "npm-publish" || tool === "strray-release";
783
704
  const postProcessorContext = {
784
705
  directory,
785
706
  operation: tool,
786
707
  filePath: args?.filePath,
787
708
  success: result?.success !== false,
788
709
  metadata: {
789
- isPublishing: isPublishOperation,
710
+ isPublishing: isPublishOperation(tool),
790
711
  hook: "tool_execution",
791
712
  toolName: tool,
792
713
  timestamp: Date.now(),
793
714
  },
794
715
  };
795
716
  const postResults = await processorManager.executePostProcessors(tool, postProcessorContext, []);
796
- // postResults is an array of ProcessorResult
797
- const allSuccess = postResults.every((r) => r.success);
798
- logger.log(`📊 Post-processor result: ${allSuccess ? "SUCCESS" : "FAILED"} (${postResults.length} processors)`);
799
- // Log each post-processor result for debugging
800
- for (const r of postResults) {
801
- if (r.success) {
802
- logger.log(`✅ Post-processor ${r.processorName}: OK`);
803
- }
804
- else {
805
- logger.error(`❌ Post-processor ${r.processorName} failed: ${r.error}`);
806
- }
807
- }
808
- // Log testAutoCreation results specifically
809
- const testAutoResult = postResults.find((r) => r.processorName === "testAutoCreation");
810
- if (testAutoResult) {
811
- if (testAutoResult.success && testAutoResult.testCreated) {
812
- logger.log(`✅ TEST AUTO-CREATION: Created ${testAutoResult.testFile}`);
813
- }
814
- else if (!testAutoResult.success) {
815
- logger.log(`â„šī¸ TEST AUTO-CREATION: ${testAutoResult.message || "skipped - no new files"}`);
816
- }
817
- }
717
+ logPostProcessorResults(postResults, logger);
718
+ logTestAutoCreationResult(postResults, logger);
818
719
  }
819
720
  catch (error) {
820
721
  logger.error(`đŸ’Ĩ Post-processor error`, error);
821
722
  }
822
723
  }
823
- // Auto inference tuning: every INFERENCE_TUNE_INTERVAL tool calls,
824
- // run a single tuning cycle to close the feedback loop.
825
724
  _openCodeToolCallCount++;
826
725
  if (_openCodeToolCallCount - _lastTuneToolCallCount >= INFERENCE_TUNE_INTERVAL) {
827
726
  _lastTuneToolCallCount = _openCodeToolCallCount;
@@ -841,17 +740,71 @@ export default async function strrayCodexPlugin(input) {
841
740
  }
842
741
  }
843
742
  },
743
+ "chat.message": async (input, output) => {
744
+ const logger = await getOrCreateLogger(directory);
745
+ if (!output.parts) {
746
+ return;
747
+ }
748
+ const textContent = input.parts?.find(p => p.type === "text")?.text ?? "";
749
+ if (!textContent) {
750
+ return;
751
+ }
752
+ const agentMentionRegex = /@(\w+)(?:\s+(.+?))?(?=$|\n\n|\r\r)/g;
753
+ let match;
754
+ let hasAgentMention = false;
755
+ let transformedText = textContent;
756
+ const knownAgents = {
757
+ "architect": "architect",
758
+ "strategist": "strategist",
759
+ "testing-lead": "testing-lead",
760
+ "bug-triage-specialist": "bug-triage-specialist",
761
+ "code-reviewer": "code-reviewer",
762
+ "security-auditor": "security-auditor",
763
+ "refactorer": "refactorer",
764
+ "researcher": "researcher",
765
+ "code-analyzer": "code-analyzer",
766
+ "frontend-engineer": "frontend-engineer",
767
+ "frontend-ui-ux-engineer": "frontend-ui-ux-engineer",
768
+ "backend-engineer": "backend-engineer",
769
+ "database-engineer": "database-engineer",
770
+ "devops-engineer": "devops-engineer",
771
+ "performance-engineer": "performance-engineer",
772
+ "mobile-developer": "mobile-developer",
773
+ "content-creator": "content-creator",
774
+ "growth-strategist": "growth-strategist",
775
+ "seo-consultant": "seo-consultant",
776
+ "tech-writer": "tech-writer",
777
+ "multimodal-looker": "multimodal-looker",
778
+ "log-monitor": "log-monitor",
779
+ };
780
+ while ((match = agentMentionRegex.exec(textContent)) !== null) {
781
+ const agentName = match[1].toLowerCase().replace(/-/g, "");
782
+ const taskPart = match[2]?.trim() ?? "";
783
+ if (knownAgents[agentName]) {
784
+ hasAgentMention = true;
785
+ const canonicalAgent = knownAgents[agentName];
786
+ logger.log(`đŸŽ¯ Agent mention detected: @${canonicalAgent}`);
787
+ const prefix = `\n[DELEGATE TO AGENT: ${canonicalAgent}]\n`;
788
+ transformedText = prefix + (taskPart || textContent.replace(`@${match[1]}`, "").trim());
789
+ break;
790
+ }
791
+ }
792
+ if (hasAgentMention) {
793
+ const textPart = output.parts.find(p => p.type === "text");
794
+ if (textPart) {
795
+ textPart.text = transformedText;
796
+ logger.log(`✅ Transformed prompt for agent routing`);
797
+ }
798
+ }
799
+ },
844
800
  config: async (_config) => {
845
- // Guard: only run init.sh once per startup session
846
- // OpenCode may fire the config hook multiple times in fresh contexts
847
- // where globalThis is reset, so we use a TTL lockfile
848
801
  const lockFile = path.join(directory, ".opencode", "logs", ".strray-init.lock");
849
802
  const now = Date.now();
850
803
  try {
851
804
  if (fs.existsSync(lockFile)) {
852
805
  const stat = fs.statSync(lockFile);
853
806
  if (now - stat.mtimeMs < 15000) {
854
- return; // already ran within 15s window
807
+ return;
855
808
  }
856
809
  }
857
810
  fs.writeFileSync(lockFile, String(now));
@@ -860,10 +813,7 @@ export default async function strrayCodexPlugin(input) {
860
813
  // lock check failed — proceed anyway
861
814
  }
862
815
  const logger = await getOrCreateLogger(directory);
863
- logger.log("🔧 Plugin config hook triggered - initializing StrRay integration");
864
- // Initialize StrRay framework
865
- // Primary: project .opencode/init.sh (copied by postinstall)
866
- // Fallback: package .opencode/init.sh (works when postinstall skips for Hermes consumers)
816
+ logger.log("🔧 Plugin config hook triggered - initializing 0xRay integration");
867
817
  let initScriptPath = path.join(directory, ".opencode", "init.sh");
868
818
  const pkgInitPath = path.join(directory, "node_modules", "strray-ai", ".opencode", "init.sh");
869
819
  if (!fs.existsSync(initScriptPath) && fs.existsSync(pkgInitPath)) {
@@ -876,7 +826,7 @@ export default async function strrayCodexPlugin(input) {
876
826
  logger.error(`Framework init error: ${stderr}`);
877
827
  }
878
828
  else {
879
- logger.log("✅ StrRay Framework initialized successfully");
829
+ logger.log("✅ 0xRay Framework initialized successfully");
880
830
  }
881
831
  }
882
832
  catch (error) {