diff --git a/.opencode/plugins/strray-codex-injection.js b/.opencode/plugins/strray-codex-injection.js index 5c6f0d4c0..7fe3f7153 100644 --- a/.opencode/plugins/strray-codex-injection.js +++ b/.opencode/plugins/strray-codex-injection.js @@ -11,6 +11,7 @@ import * as fs from "fs"; import * as path from "path"; import { spawn } from "child_process"; +import { frameworkLogger } from "../core/framework-logger.js"; // Dynamic imports for config-paths (works from both dist/plugin/ and .opencode/plugins/) let _resolveCodexPath; let _resolveStateDir; @@ -33,7 +34,7 @@ async function loadConfigPaths() { // try next candidate } } - console.warn("⚠️ Failed to load config-paths module from any location"); + frameworkLogger.log("strray-codex-plugin", "config-paths-load-failed", "warning", { warning: "Failed to load config-paths module from any location" }); } /** Convenience wrapper — must be awaited before use */ async function resolveCodexPath(...args) { @@ -62,7 +63,7 @@ async function importSystemPromptGenerator() { // try next candidate } } - console.warn("⚠️ Failed to load lean system prompt generator, using fallback"); + frameworkLogger.log("strray-codex-plugin", "system-prompt-generator-load-failed", "warning", { warning: "Failed to load lean system prompt generator, using fallback" }); } } let ProcessorManager; diff --git a/dist/state/state-manager.d.ts.map b/dist/state/state-manager.d.ts.map index 721c1d5a0..7a48be5cd 100644 --- a/dist/state/state-manager.d.ts.map +++ b/dist/state/state-manager.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"state-manager.d.ts","sourceRoot":"","sources":["../../src/state/state-manager.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC,GAAG,SAAS,CAAC;IACvC,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IACxC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAID,qBAAa,qBAAsB,YAAW,YAAY;IACxD,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,kBAAkB,CAAU;IACpC,OAAO,CAAC,UAAU,CAAqC;IACvD,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,oBAAoB,CAAgB;IAE5C,MAAM,CAAC,QAAQ,CAAC,OAAO,WAAW;gBAEtB,eAAe,SAAoB,EAAE,kBAAkB,UAAO;YAM5D,qBAAqB;YAyErB,aAAa;IAuB3B,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,mBAAmB;IAe3B,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IASlC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI;IA4BnC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IA6BxB,QAAQ,IAAI,IAAI;IA0BhB,oBAAoB,IAAI,OAAO;IAK/B,mBAAmB,IAAI;QACrB,OAAO,EAAE,OAAO,CAAC;QACjB,WAAW,EAAE,OAAO,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,aAAa,EAAE,MAAM,CAAC;KACvB;IAUD,eAAe,IAAI,MAAM;IAIzB,WAAW,IAAI,KAAK,CAAC;QAAE,SAAS,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAIzE,eAAe,CAAC,QAAQ,EAAE;QACxB,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,EAAE,OAAO,CAAC;KACjB,GAAG,OAAO;CAQZ;AAGD,OAAO,EAAE,qBAAqB,IAAI,kBAAkB,EAAE,CAAC"} \ No newline at end of file +{"version":3,"file":"state-manager.d.ts","sourceRoot":"","sources":["../../src/state/state-manager.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC,GAAG,SAAS,CAAC;IACvC,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IACxC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAID,qBAAa,qBAAsB,YAAW,YAAY;IACxD,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,kBAAkB,CAAU;IACpC,OAAO,CAAC,UAAU,CAAqC;IACvD,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,oBAAoB,CAAgB;IAE5C,MAAM,CAAC,QAAQ,CAAC,OAAO,WAAW;gBAEtB,eAAe,SAA+B,EAAE,kBAAkB,UAAO;YAMvE,qBAAqB;YAyErB,aAAa;IAuB3B,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,mBAAmB;IAe3B,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IASlC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI;IA4BnC,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IA6BxB,QAAQ,IAAI,IAAI;IA0BhB,oBAAoB,IAAI,OAAO;IAK/B,mBAAmB,IAAI;QACrB,OAAO,EAAE,OAAO,CAAC;QACjB,WAAW,EAAE,OAAO,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,aAAa,EAAE,MAAM,CAAC;KACvB;IAUD,eAAe,IAAI,MAAM;IAIzB,WAAW,IAAI,KAAK,CAAC;QAAE,SAAS,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAIzE,eAAe,CAAC,QAAQ,EAAE;QACxB,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,EAAE,OAAO,CAAC;QAChB,MAAM,EAAE,OAAO,CAAC;KACjB,GAAG,OAAO;CAQZ;AAGD,OAAO,EAAE,qBAAqB,IAAI,kBAAkB,EAAE,CAAC"} \ No newline at end of file diff --git a/dist/state/state-manager.js b/dist/state/state-manager.js index 9545474f2..70873ab34 100644 --- a/dist/state/state-manager.js +++ b/dist/state/state-manager.js @@ -7,7 +7,7 @@ export class StringRayStateManager { initialized = false; earlyOperationsQueue = []; // Queue keys that need persistence after init static VERSION = "1.5.2"; - constructor(persistencePath = ".opencode/state", persistenceEnabled = true) { + constructor(persistencePath = ".opencode/state/state.json", persistenceEnabled = true) { this.persistencePath = persistencePath; this.persistenceEnabled = persistenceEnabled; this.initializePersistence(); diff --git a/dist/state/state-manager.js.map b/dist/state/state-manager.js.map index 4f632504c..7532d83c1 100644 --- a/dist/state/state-manager.js.map +++ b/dist/state/state-manager.js.map @@ -1 +1 @@ -{"version":3,"file":"state-manager.js","sourceRoot":"","sources":["../../src/state/state-manager.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"state-manager.js","sourceRoot":"","sources":["../../src/state/state-manager.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/docs/reflections/deep/framework-hygiene-journey-2026-03-29.md b/docs/reflections/deep/framework-hygiene-journey-2026-03-29.md new file mode 100644 index 000000000..90b65947d --- /dev/null +++ b/docs/reflections/deep/framework-hygiene-journey-2026-03-29.md @@ -0,0 +1,175 @@ +# Framework Hygiene Journey: When the Framework's Own Plumbing Leaks + +**Date**: 2026-03-29 +**PR**: [#12](https://github.com/htafolla/StringRay/pull/12) — `fix/logging-persistence-enforcer-overhead` + +--- + +It started with the user asking to check a diff. + +That's always how it starts, isn't it? Something small. A subagent had made changes — the `enforcer-tools.ts` diff and a `codex-injection.js` change that the user didn't make. Just review the diff, make sure the subagent didn't break anything. Ten minutes, tops. + +I opened the diff. The enforcer changes looked clean — the `blocked` logic had been simplified from a weird keyword-matching approach to a straightforward "any error means blocked." The codex-injection plugin had two `console.warn` calls replaced with `frameworkLogger.log`. Standard stuff. + +But then the user said something that changed the trajectory of the entire session: + +> "StrRay: Loading from node_modules... generator, using fallback" + +And another: + +> "Failed to load lean system prompt generator, using fallback" + +They were seeing console output bleeding through into their OpenCode agent UI. The framework — the thing whose entire job is to enforce code quality and prevent noise — was itself the source of noise. + +## The Bleed + +I went looking for where those messages came from. The first one, `console.debug?.("StrRay: Loading from node_modules...")`, was in `~/dev/jelly/.opencode/plugin/strray-codex-injection.ts` — a stale copy of the plugin that lived in the consumer project, not in the StringRay repo at all. The second one, "Failed to load lean system prompt generator," was in `~/dev/stringray/.opencode/plugins/strray-codex-injection.js` — another stale copy in the read-only reference repo. + +Both were already fixed in the PR branch source code. The live files were just old deployments. But the user was seeing them *right now*, which meant the fix needed to get shipped. + +That led to the obvious question: are there more? How many `console.*` calls are bleeding through the framework's runtime code into agent consoles? + +The answer was: **dozens**. + +## The Counting + +I ran a grep across all of `src/` and started counting. And counting. And counting. The list kept going: + +- `security-hardening-system.ts` — 5 calls +- `processor-manager.ts` — 5 calls +- `registry.ts` — 15+ `.catch(console.error)` patterns +- `shutdown-handler.ts` — 4 calls +- `codex-parser.ts` — 2 calls +- `framework-logger.ts` itself — 3 `.catch(console.error)` calls (the logger using console to handle its own errors — recursive irony) +- Every single knowledge-skills MCP server — 15 files, each with `.catch(console.error)` at their entrypoint +- All the performance files — budget enforcer, CI gates, monitoring dashboard, regression tester +- All the postprocessor validators — comprehensive, lightweight, hook metrics +- The connection pool and connection manager + +After filtering out comments, string literals, regex patterns, detection logic that *checks* for console.log in user code, JSDoc examples, CLI output (intentional), demo scripts, and test fixtures... there were still **49 remaining** actual `console.*` calls in framework runtime code. + +49 places where the framework could silently dump noise into whatever process was running it. 49 places where, if StringRay was loaded as a plugin in OpenCode or Hermes, the user would see garbage in their agent console. + +## The Parallel Assault + +I couldn't fix 49 files one at a time. The session would time out. So I dispatched three parallel subagents: + +1. **Core + security + processors + MCPs** — the framework plumbing +2. **Postprocessor + performance + validation** — the enforcement pipeline +3. **Test fixes** — the 6 tests that were about to break + +Each subagent got a detailed brief: which files to touch, which import path to use for `frameworkLogger`, the exact API signature (`frameworkLogger.log(module, event, status, details)`), and which files to explicitly skip (detection logic, comments, CLI output, examples). + +The first subagent got through most of the core files but timed out partway through the MCP servers. The second subagent knocked out all the postprocessor and performance files cleanly. The third fixed all 6 tests by replacing `vi.spyOn(console, "log")` with `vi.spyOn(frameworkLogger, "log")`. + +I still had to manually fix two stragglers that the subagents missed — `code-analyzer.server.ts` had its `.catch(console.error)` on a single line with an if-statement, so grep didn't match it. And `architecture-patterns.server.ts` was listed as "out of scope" by a subagent that was being too conservative. + +**Lesson**: Subagents are thorough but not exhaustive. They follow instructions precisely, which means they'll skip something if your filtering instructions are too aggressive. Always do a final sweep yourself. + +## The Silent Catch + +One pattern decision that's worth calling out: for `.catch(console.error)` at the end of promise chains — especially in MCP server entrypoints and process-level shutdown handlers — I used `.catch(() => {})` instead of trying to route through `frameworkLogger`. + +The reasoning: these are terminal handlers. The MCP servers use `.catch(console.error)` at the bottom of their entrypoint script — `server.run().catch(console.error)`. If the server crashes, the process is exiting. Trying to log through `frameworkLogger` at that point could itself fail (the logger writes to disk, what if the disk is full? what if the logger is in a bad state?). Silent catch is the right call here. Die quietly. + +For `framework-logger.ts` itself — the logger's own internal error handlers — the same logic applies. The logger calls `.catch(console.error)` when `jobContext.complete()` fails. But if the logger can't log, where do you send the error? To the logger? That's a loop. Silent swallow is the only safe option. + +## The Tests + +Six tests broke. All the same pattern: they were spying on `console.log` or `console.warn` and asserting those calls happened. But we'd just removed those console calls from the source code and replaced them with `frameworkLogger.log`, which writes to file and never touches console. + +The test fixes were straightforward but tedious. For each one: +1. Add `import { frameworkLogger }` with the right relative path +2. Replace `vi.spyOn(console, "log")` with `vi.spyOn(frameworkLogger, "log").mockResolvedValue(undefined)` +3. Replace `expect(console.log).toHaveBeenCalledWith("📊 Success Metrics:")` with `expect(frameworkLogger.log).toHaveBeenCalledWith(expect.any(String), "log-metrics", expect.any(String), expect.objectContaining({}))` + +The key insight: don't be too strict on the details object in the assertion. Use `expect.objectContaining` or `expect.any(String)` for fields you don't care about. The tests are checking "did this thing happen," not "did this thing happen with exactly these parameters." Over-specifying the assertion makes it fragile. + +**127/127 test files. 2399/2399 tests. All green.** + +## The Enforcer Logic + +While the console cleanup was the bulk of the work, the enforcer-tools.ts changes deserve their own moment. + +The old `blocked` logic was: + +```typescript +blocked: !report.passed && report.errors.some( + (e) => e.includes("required") || e.includes("violation") +) +``` + +Think about what that does. An error like "missing semicolon" — real error, real problem — would *not* block because the string "missing semicolon" doesn't contain "required" or "violation". The enforcer would see the error, log it, report it... and then let it through. The enforcement was only catching errors that happened to use specific magic words. + +The fix is almost embarrassingly simple: `blocked: report.errors.length > 0`. Any error means blocked. Period. + +There was also a double-blocking issue in `contextAnalysisValidation` — it was combining `validationResult.blocked` with `contextIssues.errors.length > 0`, but `validationResult.blocked` already accounted for the errors from the inner validation. So you'd get blocked twice for the same error, which is harmless but inelegant. + +And in `codexEnforcement`, the result was built by spreading `...validationResult` (which had its own `blocked` value) and then setting `errors` and `warnings` from both sources — but never updating `blocked` to account for the new codex violations being added. So codex violations could be present in the errors array but `blocked` would still be `false` if the inner validation passed. That's a real bug. + +## The State Persistence Bug + +This one's subtle. `StateManager` had: + +```typescript +constructor(persistencePath = ".opencode/state", persistenceEnabled = true) +``` + +That path is a *directory*, not a file. When `initializePersistence()` calls `writeFileSync(this.persistencePath, ...)`, it doesn't write *into* that directory — it creates a *file* at that exact path. So you'd get a file literally named `state` in `.opencode/`, not `state.json` inside `.opencode/state/`. + +The fix added `resolveStateFilePath()` to `config-paths.ts` and changed the default to `.opencode/state/state.json`. Clean, surgical, but the kind of bug that would drive someone insane trying to figure out why state isn't persisting. + +## The Subagent Enforcement Gap + +Here's where the session took an unexpected turn. + +After all the cleanup was done, the user asked a simple question: "is the subagent operating in the StringRay framework, aka plugin in Hermes, as it should follow the rules?" + +The answer was no. `delegate_task` spawns an isolated Hermes subagent with its own terminal session and toolset. The StringRay plugin hooks — `pre_tool_call`, `post_tool_call`, the quality gates, the codex checks — only fire for the **main agent's** tool calls. Subagents bypass all of it. + +That means the three subagents I'd just dispatched to fix 30+ files? None of them went through a quality gate. None of them had their output validated. They just wrote to the filesystem and returned. I was trusting them to do it right because I gave them detailed instructions, but there was no mechanical enforcement. + +That's a real gap. In an autonomous system, trust without verification is a liability. + +So I built the enforcement mechanism. The approach is post-hoc: you can't prevent the subagent from doing something wrong (it's already running in isolation), but you can check what it did after it returns. + +The implementation: + +1. **`pre_tool_call`**: When `delegate_task` fires, snapshot the working tree by running `git diff --name-only HEAD`. Store the baseline keyed by task_id. +2. **`post_tool_call`**: When the subagent returns, re-run the diff, compute the delta (new changes = after - before), filter to source files, and validate every changed file through the bridge quality gate. + +It's not perfect — it's post-hoc, so the damage is already done if the subagent wrote bad code. But it's logged, and the parent agent can see the violations in `activity.log`. It's better than nothing, and it's the only architecture that works given that subagents are black boxes. + +The tricky part was the canonical source location. I initially modified `~/.hermes/plugins/strray-hermes/__init__.py` — the live deployed plugin — before realizing that the canonical source is `src/integrations/hermes-agent/__init__.py` in the StringRay repo. The postinstall script copies from there to `~/.hermes/plugins/`. Modifying the deployed copy without updating the source means the fix would be lost on the next `npm install`. + +And I almost created a `plugins/strray-hermes/` directory in the repo root before the user pointed out the correct location. That would have been a phantom directory with no connection to anything. + +## The Commit + +The user asked me to look at everything and make sure it was all captured properly. I'd written two commits with narrow messages — "eliminate all console.* bleed-through" and "add subagent enforcement." But the PR contained five distinct changes, and those commit messages only described two of them. + +The user noticed. "We did more than fix console bleed this session." + +I squashed everything into one commit with a detailed body covering all five changes. Not five commits for five changes — one commit, one clean message, because these changes were all discovered and fixed in the same investigative session. They're one story, not five. + +## What I'd Do Different + +The subagent narration issue. When the first subagent was running, it output things like "The lint errors are pre-existing TS config issues, not related to my change." The user called that out: "you can not say this. you are the dev." Subagents shouldn't be narrating excuses. They should fix the problem or flag it properly. That's a prompt discipline issue — I need to be more explicit in my subagent instructions about output style. + +The canonical source confusion. I should have checked where the Hermes plugin lived in the repo *before* modifying the deployed copy. One `grep -rn "hermesPluginSource" scripts/node/postinstall.cjs` would have shown me the source is at `src/integrations/hermes-agent/`. Instead I went straight to `~/.hermes/plugins/` and started editing. + +The commit messages. I should have been reviewing the full scope of changes against the commit messages as I went, not at the end. The pattern of "fix thing, commit, fix next thing, commit" leads to fragmented history that doesn't tell the story. + +## What This Means + +The StringRay framework's console bleed-through was a fundamental hygiene issue. The framework enforces codex rules on consumer code — including "no console.log" — while its own runtime was full of console.* calls. That's the kind of inconsistency that destroys trust. You can't enforce rules you don't follow yourself. + +The subagent enforcement gap is more structural. StringRay was designed as a plugin system for a single agent context. Multi-agent orchestration via `delegate_task` is a newer capability that the plugin architecture doesn't natively support. The post-hoc validation approach is a pragmatic compromise — not architecturally elegant, but it closes the gap without requiring a fundamental rethinking of the plugin model. + +The state persistence bug and the enforcer logic bug are the kind of issues that work for years without being noticed because they fail silently. State writes to the wrong path — you get a file where you expect a directory — and nothing obviously breaks. The enforcer lets errors through because the error message doesn't contain the magic word — nothing crashes, the code just isn't enforced properly. + +Silent failures are the most dangerous failures, because they don't generate bug reports. + +--- + +*48 files changed. 362 insertions, 1,125 deletions. One commit. One PR. But the journey touched five distinct bugs, a new feature, an architectural gap, and a lesson about trust without verification.* diff --git a/performance-baselines.json b/performance-baselines.json index fd729bc1b..702f8a89a 100644 --- a/performance-baselines.json +++ b/performance-baselines.json @@ -25,10 +25,10 @@ }, "test-execution-performance": { "testName": "test-execution-performance", - "averageDuration": 0.10210764407894761, - "standardDeviation": 0.009411626785579734, - "sampleCount": 1520, - "lastUpdated": 1774738297194, + "averageDuration": 0.10210839001969822, + "standardDeviation": 0.009403422591239437, + "sampleCount": 1523, + "lastUpdated": 1774793356236, "tolerance": 10 } } \ No newline at end of file diff --git a/plugins/strray-codex-injection.js b/plugins/strray-codex-injection.js deleted file mode 100644 index e8d359898..000000000 --- a/plugins/strray-codex-injection.js +++ /dev/null @@ -1,942 +0,0 @@ -/** - * StrRay Codex Injection Plugin for OpenCode - * - * This plugin automatically injects the Universal Development Codex - * into the system prompt for all AI agents, ensuring codex terms are - * consistently enforced across the entire development session. - * - * @author StrRay Framework - */ -import * as fs from "fs"; -import * as path from "path"; -import { spawn } from "child_process"; -// Dynamic imports with absolute paths at runtime -let runQualityGateWithLogging; -let qualityGateDirectory = ""; -async function importQualityGate(directory) { - if (!runQualityGateWithLogging || qualityGateDirectory !== directory) { - try { - const qualityGatePath = path.join(directory, "dist", "plugin", "quality-gate.js"); - const module = await import(qualityGatePath); - runQualityGateWithLogging = module.runQualityGateWithLogging; - qualityGateDirectory = directory; - } - catch (e) { - // Quality gate not available - } - } -} -// Direct activity logging - writes to activity.log without module isolation issues -let activityLogPath = ""; -let activityLogInitialized = false; -function initializeActivityLog(directory) { - if (activityLogInitialized && activityLogPath) - return; - const logDir = path.join(directory, "logs", "framework"); - if (!fs.existsSync(logDir)) { - fs.mkdirSync(logDir, { recursive: true }); - } - // Use a separate file for plugin tool events to avoid framework overwrites - activityLogPath = path.join(logDir, "plugin-tool-events.log"); - activityLogInitialized = true; -} -function logToolActivity(directory, eventType, tool, args, result, error, duration) { - initializeActivityLog(directory); - const timestamp = new Date().toISOString(); - const jobId = `plugin-${Date.now()}-${Math.random().toString(36).substring(2, 8)}`; - if (eventType === "start") { - const entry = `${timestamp} [${jobId}] [agent] tool-started - INFO | {"tool":"${tool}","args":${JSON.stringify(Object.keys(args || {}))}}\n`; - fs.appendFileSync(activityLogPath, entry); - } - else if (eventType === "routing") { - const entry = `${timestamp} [${jobId}] [agent] routing-detected - INFO | {"tool":"${tool}","routing":${JSON.stringify(args)}}\n`; - fs.appendFileSync(activityLogPath, entry); - } - else { - const success = !error; - const level = success ? "SUCCESS" : "ERROR"; - const entry = `${timestamp} [${jobId}] [agent] tool-${success ? "complete" : "failed"} - ${level} | {"tool":"${tool}","duration":${duration || 0}${error ? `,"error":"${error}"` : ""}}\n`; - fs.appendFileSync(activityLogPath, entry); - } -} -// Import lean system prompt generator -let SystemPromptGenerator; -async function importSystemPromptGenerator() { - if (!SystemPromptGenerator) { - try { - const module = await import("../core/system-prompt-generator.js"); - SystemPromptGenerator = module.generateLeanSystemPrompt; - } - catch (e) { - // Fallback to original implementation - silent fail - } - } -} -let ProcessorManager; -let StrRayStateManager; -let featuresConfigLoader; -let detectTaskType; -let TaskSkillRouter; -let taskSkillRouterInstance; -async function loadStrRayComponents() { - if (ProcessorManager && StrRayStateManager && featuresConfigLoader) { - return; - } - const tempLogger = await getOrCreateLogger(process.cwd()); - tempLogger.log(`[StrRay] 🔄 loadStrRayComponents() called - attempting to load framework components`); - // Try local dist first (for development) - try { - tempLogger.log(`[StrRay] 🔄 Attempting to load from ../../dist/`); - const procModule = await import("../../dist/processors/processor-manager.js"); - const stateModule = await import("../../dist/state/state-manager.js"); - const featuresModule = await import("../../dist/core/features-config.js"); - ProcessorManager = procModule.ProcessorManager; - StrRayStateManager = stateModule.StrRayStateManager; - featuresConfigLoader = featuresModule.featuresConfigLoader; - detectTaskType = featuresModule.detectTaskType; - tempLogger.log(`[StrRay] ✅ Loaded from ../../dist/`); - return; - } - catch (e) { - tempLogger.error(`[StrRay] ❌ Failed to load from ../../dist/: ${e?.message || e}`); - } - // Try node_modules (for consumer installation) - const pluginPaths = ["strray-ai", "strray-framework"]; - for (const pluginPath of pluginPaths) { - try { - tempLogger.log(`[StrRay] 🔄 Attempting to load from ../../node_modules/${pluginPath}/dist/`); - const pm = await import(`../../node_modules/${pluginPath}/dist/processors/processor-manager.js`); - const sm = await import(`../../node_modules/${pluginPath}/dist/state/state-manager.js`); - const fm = await import(`../../node_modules/${pluginPath}/dist/core/features-config.js`); - ProcessorManager = pm.ProcessorManager; - StrRayStateManager = sm.StrRayStateManager; - featuresConfigLoader = fm.featuresConfigLoader; - detectTaskType = fm.detectTaskType; - tempLogger.log(`[StrRay] ✅ Loaded from ../../node_modules/${pluginPath}/dist/`); - return; - } - catch (e) { - tempLogger.error(`[StrRay] ❌ Failed to load from ../../node_modules/${pluginPath}/dist/: ${e?.message || e}`); - continue; - } - } - tempLogger.error(`[StrRay] ❌ Could not load StrRay components from any path`); -} -/** - * Extract task description from tool input - */ -function extractTaskDescription(input) { - const { tool, args } = input; - // Extract meaningful task description from various inputs - if (args?.content) { - const content = String(args.content); - // Get first 200 chars as description - return content.slice(0, 200); - } - if (args?.filePath) { - return `${tool} ${args.filePath}`; - } - if (args?.command) { - return String(args.command); - } - // Fallback: Use tool name as task description for routing - // This enables routing even when OpenCode doesn't pass args - if (tool) { - return `execute ${tool} tool`; - } - return null; -} -/** - * Extract action words from command for better routing - * Maps verbs/intents to skill categories - */ -function extractActionWords(command) { - if (!command || command.length < 3) - return null; - // Strip quotes and escape sequences for cleaner matching - const cleanCommand = command.replace(/["']/g, ' ').replace(/\\./g, ' '); - // Action word -> skill mapping (ordered by priority) - const actionMap = [ - // Review patterns - check first since user likely wants to review content - { pattern: /\b(review|check|audit|examine|inspect|assess|evaluate)\b/i, skill: "code-review" }, - // Analyze patterns - { pattern: /\b(analyze|investigate|study)\b/i, skill: "code-analyzer" }, - // Fix patterns - { pattern: /\b(fix|debug|resolve|troubleshoot|repair)\b/i, skill: "bug-triage" }, - // Create patterns - { pattern: /\b(create|write|generate|build|make|add)\b/i, skill: "content-creator" }, - // Test patterns - { pattern: /\b(test|validate|verify)\b/i, skill: "testing" }, - // Design patterns - { pattern: /\b(design|plan|architect)\b/i, skill: "architecture" }, - // Optimize patterns - { pattern: /\b(optimize|improve|enhance|speed)\b/i, skill: "performance" }, - // Security patterns - { pattern: /\b(scan|secure|vulnerability)\b/i, skill: "security" }, - // Refactor patterns - { pattern: /\b(refactor|clean|restructure)\b/i, skill: "refactoring" }, - ]; - // Search for action words anywhere in the command - for (const { pattern } of actionMap) { - const match = cleanCommand.match(pattern); - if (match) { - // Return the matched word plus context after it - const word = match[0]; - const idx = cleanCommand.toLowerCase().indexOf(word.toLowerCase()); - const after = cleanCommand.slice(idx + word.length, Math.min(idx + word.length + 25, cleanCommand.length)).trim(); - return `${word} ${after}`.trim().slice(0, 40); - } - } - // If no action word found, return null to use default routing - return null; -} -/** - * Estimate complexity score based on message content - * Higher complexity = orchestrator routing - * Lower complexity = code-reviewer routing - */ -function estimateComplexity(message) { - const text = message.toLowerCase(); - // High complexity indicators - const highComplexityKeywords = [ - "architecture", "system", "design", "complex", "multiple", - "integrate", "database", "migration", "refactor", - "performance", "optimize", "security", "audit", - "orchestrate", "coordinate", "workflow" - ]; - // Low complexity indicators - const lowComplexityKeywords = [ - "review", "check", "simple", "quick", "fix", - "small", "typo", "format", "lint", "test" - ]; - let score = 50; // default medium - // Check message length - if (message.length > 200) - score += 10; - if (message.length > 500) - score += 15; - // Check for high complexity keywords - for (const keyword of highComplexityKeywords) { - if (text.includes(keyword)) - score += 8; - } - // Check for low complexity keywords - for (const keyword of lowComplexityKeywords) { - if (text.includes(keyword)) - score -= 5; - } - // Clamp to 0-100 - return Math.max(0, Math.min(100, score)); -} -async function loadTaskSkillRouter() { - if (taskSkillRouterInstance) { - return; // Already loaded - } - // Try local dist first (for development) - try { - const module = await import("../../dist/delegation/task-skill-router.js"); - TaskSkillRouter = module.TaskSkillRouter; - taskSkillRouterInstance = new TaskSkillRouter(); - } - catch (distError) { - // Try node_modules (for consumer installs) - try { - const module = await import("strray-ai/dist/delegation/task-skill-router.js"); - TaskSkillRouter = module.TaskSkillRouter; - taskSkillRouterInstance = new TaskSkillRouter(); - } - catch (nmError) { - // Task routing not available - continue without it - } - } -} -function spawnPromise(command, args, cwd) { - return new Promise((resolve, reject) => { - const child = spawn(command, args, { - cwd, - stdio: ["ignore", "pipe", "pipe"], - }); - let stdout = ""; - let stderr = ""; - if (child.stdout) { - child.stdout.on("data", (data) => { - const text = data.toString(); - stdout += text; - process.stdout.write(text); - }); - } - if (child.stderr) { - child.stderr.on("data", (data) => { - stderr += data.toString(); - }); - } - child.on("close", (code) => { - if (code === 0) { - resolve({ stdout, stderr }); - } - else { - reject(new Error(`Process exited with code ${code}: ${stderr}`)); - } - }); - child.on("error", (error) => { - reject(error); - }); - }); -} -class PluginLogger { - logPath; - constructor(directory) { - const logsDir = path.join(directory, ".opencode", "logs"); - if (!fs.existsSync(logsDir)) { - fs.mkdirSync(logsDir, { recursive: true }); - } - const today = new Date().toISOString().split("T")[0]; - this.logPath = path.join(logsDir, `strray-plugin-${today}.log`); - } - async logAsync(message) { - try { - const timestamp = new Date().toISOString(); - const logEntry = `[${timestamp}] ${message}\n`; - await fs.promises.appendFile(this.logPath, logEntry, "utf-8"); - } - catch (error) { - // Silent fail - logging failure should not break plugin - } - } - log(message) { - void this.logAsync(message); - } - error(message, error) { - const errorDetail = error instanceof Error ? `: ${error.message}` : ""; - this.log(`ERROR: ${message}${errorDetail}`); - } -} -let loggerInstance = null; -let loggerInitPromise = null; -async function getOrCreateLogger(directory) { - if (loggerInstance) { - return loggerInstance; - } - if (loggerInitPromise) { - return loggerInitPromise; - } - loggerInitPromise = (async () => { - const logger = new PluginLogger(directory); - loggerInstance = logger; - return logger; - })(); - return loggerInitPromise; -} -/** - * Get the current framework version from package.json - */ -function getFrameworkVersion() { - try { - const packageJsonPath = path.join(process.cwd(), "package.json"); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); - return packageJson.version || "1.4.6"; - } - catch { - return "1.4.6"; - } -} -/** - * Get lean framework identity message (token-efficient version) - */ -function getFrameworkIdentity() { - const version = getFrameworkVersion(); - return `StringRay Framework v${version} - AI Orchestration - -🔧 Core: enforcer, architect, orchestrator, code-reviewer, refactorer, testing-lead -📚 Codex: 5 Essential Terms (99.6% Error Prevention Target) -🎯 Goal: Progressive, production-ready development workflow - -📖 Documentation: .opencode/strray/ (codex, config, agents docs) -`; -} -/** - * Global codex context cache (loaded once) - */ -let cachedCodexContexts = null; -/** - * Codex file locations to search - */ -const CODEX_FILE_LOCATIONS = [ - ".opencode/strray/codex.json", - ".opencode/codex.codex", - ".opencode/strray/agents_template.md", - "AGENTS.md", -]; -/** - * Read file content safely - */ -function readFileContent(filePath) { - try { - return fs.readFileSync(filePath, "utf-8"); - } - catch (error) { - const logger = new PluginLogger(process.cwd()); - logger.error(`Failed to read file ${filePath}`, error); - return null; - } -} -/** - * Extract codex metadata from content - */ -function extractCodexMetadata(content) { - // Try JSON format first (codex.json) - if (content.trim().startsWith("{")) { - try { - const parsed = JSON.parse(content); - const version = parsed.version || "1.6.0"; - const terms = parsed.terms || {}; - const termCount = Object.keys(terms).length; - return { version, termCount }; - } - catch { - // Not valid JSON, try markdown format - } - } - // Markdown format (AGENTS.md, .opencode/strray/agents_template.md) - const versionMatch = content.match(/\*\*Version\*\*:\s*(\d+\.\d+\.\d+)/); - const version = versionMatch && versionMatch[1] ? versionMatch[1] : "1.6.0"; - const termMatches = content.match(/####\s*\d+\.\s/g); - const termCount = termMatches ? termMatches.length : 0; - return { version, termCount }; -} -/** - * Create codex context entry - */ -function createCodexContextEntry(filePath, content) { - const metadata = extractCodexMetadata(content); - return { - id: `strray-codex-${path.basename(filePath)}`, - source: filePath, - content, - priority: "critical", - metadata: { - version: metadata.version, - termCount: metadata.termCount, - loadedAt: new Date().toISOString(), - }, - }; -} -/** - * Load codex context (cached globally, loaded once) - */ -function loadCodexContext(directory) { - if (cachedCodexContexts) { - return cachedCodexContexts; - } - const codexContexts = []; - for (const relativePath of CODEX_FILE_LOCATIONS) { - const fullPath = path.join(directory, relativePath); - const content = readFileContent(fullPath); - if (content && content.trim().length > 0) { - const entry = createCodexContextEntry(fullPath, content); - if (entry.metadata.termCount > 0) { - codexContexts.push(entry); - } - } - } - cachedCodexContexts = codexContexts; - if (codexContexts.length === 0) { - void getOrCreateLogger(directory).then((l) => l.error(`No valid codex files found. Checked: ${CODEX_FILE_LOCATIONS.join(", ")}`)); - } - return codexContexts; -} -/** - * Format codex context for injection - */ -function formatCodexContext(contexts) { - if (contexts.length === 0) { - return ""; - } - const parts = []; - for (const context of contexts) { - parts.push(`# StrRay Codex Context v${context.metadata.version}`, `Source: ${context.source}`, `Terms Loaded: ${context.metadata.termCount}`, `Loaded At: ${context.metadata.loadedAt}`, "", context.content, "", "---", ""); - } - return parts.join("\n"); -} -/** - * Main plugin function - * - * This plugin hooks into experimental.chat.system.transform event - * to inject codex terms into system prompt before it's sent to LLM. - * - * OpenCode expects hooks to be nested under a "hooks" key. - */ -export default async function strrayCodexPlugin(input) { - const { directory: inputDirectory } = input; - const directory = inputDirectory || process.cwd(); - return { - "experimental.chat.system.transform": async (_input, output) => { - try { - await importSystemPromptGenerator(); - let leanPrompt = getFrameworkIdentity(); - if (SystemPromptGenerator) { - leanPrompt = await SystemPromptGenerator({ - showWelcomeBanner: true, - showCodexContext: false, - enableTokenOptimization: true, - maxTokenBudget: 3000, - showCriticalTermsOnly: true, - showEssentialLinks: true - }); - } - // Routing is handled in chat.message hook - this hook only does system prompt injection - if (output.system && Array.isArray(output.system)) { - output.system = [leanPrompt]; - } - } - catch (error) { - const logger = await getOrCreateLogger(directory); - logger.error("System prompt injection failed:", error); - const fallback = getFrameworkIdentity(); - if (output.system && Array.isArray(output.system)) { - output.system = [fallback]; - } - } - }, - "tool.execute.before": async (input, output) => { - const logger = await getOrCreateLogger(directory); - // Retrieve original user message for context preservation (file-based) - let originalMessage = null; - try { - const contextDir = path.join(directory, ".opencode", "logs"); - if (fs.existsSync(contextDir)) { - const contextFiles = fs.readdirSync(contextDir) - .filter(f => f.startsWith("context-") && f.endsWith(".json")) - .map(f => ({ - name: f, - time: fs.statSync(path.join(contextDir, f)).mtime.getTime() - })) - .sort((a, b) => b.time - a.time); - if (contextFiles.length > 0 && contextFiles[0]) { - const latestContext = JSON.parse(fs.readFileSync(path.join(contextDir, contextFiles[0].name), "utf-8")); - originalMessage = latestContext.userMessage; - } - } - } - catch (e) { - // Silent fail - } - if (originalMessage) { - logger.log(`📌 Original intent: "${originalMessage.slice(0, 80)}..."`); - } - // Log tool start to activity logger (direct write - no module isolation issues) - logToolActivity(directory, "start", input.tool, input.args || {}); - await loadStrRayComponents(); - if (featuresConfigLoader && detectTaskType) { - try { - const config = featuresConfigLoader.loadConfig(); - if (config.model_routing?.enabled) { - const taskType = detectTaskType(input.tool); - const routing = config.model_routing.task_routing?.[taskType]; - if (routing?.model) { - output.model = routing.model; - logger.log(`Model routed: ${input.tool} → ${taskType} → ${routing.model}`); - } - } - } - catch (e) { - logger.error("Model routing error", e); - } - } - const { tool, args } = input; - // Extract action words from command for better tool routing - const command = args?.command ? String(args.command) : ""; - let taskDescription = null; - if (command) { - const actionWords = extractActionWords(command); - if (actionWords) { - taskDescription = actionWords; - logger.log(`📝 Action words extracted: "${actionWords}"`); - } - } - // Also try to extract from content if no command - if (!taskDescription) { - taskDescription = extractTaskDescription(input); - } - // Route tool commands based on extracted action words - if (taskDescription) { - try { - await loadTaskSkillRouter(); - if (taskSkillRouterInstance) { - const routingResult = taskSkillRouterInstance.routeTask(taskDescription, { - source: "tool_command", - complexity: estimateComplexity(taskDescription), - }); - if (routingResult && routingResult.agent) { - logger.log(`🎯 Tool routed: ${tool} → @${routingResult.agent} (${Math.round(routingResult.confidence * 100)}%)`); - // Log routing for analytics - logToolActivity(directory, "routing", tool, { - taskDescription, - agent: routingResult.agent, - confidence: routingResult.confidence - }); - } - } - } - catch (e) { - // Silent fail - routing should not break tool execution - logger.log(`📝 Tool routing skipped: ${e}`); - } - } - // ENFORCER QUALITY GATE CHECK - Block on violations - await importQualityGate(directory); - if (!runQualityGateWithLogging) { - logger.log("Quality gate not available, skipping"); - } - else { - const qualityGateResult = await runQualityGateWithLogging({ tool, args }, logger); - if (!qualityGateResult.passed) { - logger.error(`🚫 Quality gate failed: ${qualityGateResult.violations.join(", ")}`); - throw new Error(`ENFORCER BLOCKED: ${qualityGateResult.violations.join("; ")}`); - } - } - // Run processors for ALL tools (not just write/edit) - if (ProcessorManager || StrRayStateManager) { - // PHASE 1: Connect to booted framework or boot if needed - let stateManager; - let processorManager; - // Check if framework is already booted (global state exists) - const globalState = globalThis.strRayStateManager; - if (globalState) { - logger.log("🔗 Connecting to booted StrRay framework"); - stateManager = globalState; - } - else { - logger.log("🚀 StrRay framework not booted, initializing..."); - // Create new state manager (framework not booted yet) - stateManager = new StrRayStateManager(path.join(directory, ".opencode", "state")); - // Store globally for future use - globalThis.strRayStateManager = stateManager; - } - // Get processor manager from state - processorManager = stateManager.get("processor:manager"); - if (!processorManager) { - logger.log("⚙️ Creating and registering processors..."); - processorManager = new ProcessorManager(stateManager); - // Register the same processors as boot-orchestrator - processorManager.registerProcessor({ - name: "preValidate", - type: "pre", - priority: 10, - enabled: true, - }); - processorManager.registerProcessor({ - name: "codexCompliance", - type: "pre", - priority: 20, - enabled: true, - }); - processorManager.registerProcessor({ - name: "versionCompliance", - type: "pre", - priority: 25, - enabled: true, - }); - processorManager.registerProcessor({ - name: "testAutoCreation", - type: "post", - priority: 5, // FIX: Run BEFORE testExecution so tests exist when we run them - enabled: true, - }); - processorManager.registerProcessor({ - name: "testExecution", - type: "post", - priority: 10, - enabled: true, - }); - processorManager.registerProcessor({ - name: "coverageAnalysis", - type: "post", - priority: 20, - enabled: true, - }); - // Store for future use - stateManager.set("processor:manager", processorManager); - logger.log("✅ Processors registered successfully"); - } - else { - logger.log("✅ Using existing processor manager"); - } - // PHASE 2: Execute pre-processors with detailed logging - try { - // Check if processorManager and method exist - if (!processorManager || typeof processorManager.executePreProcessors !== 'function') { - logger.log(`⏭️ Pre-processors skipped: processor manager not available`); - return; - } - logger.log(`▶️ Executing pre-processors for ${tool}...`); - const result = await processorManager.executePreProcessors({ - tool, - args, - context: { - directory, - operation: "tool_execution", - filePath: args?.filePath, - }, - }); - logger.log(`📊 Pre-processor result: ${result.success ? "SUCCESS" : "FAILED"} (${result.results?.length || 0} processors)`); - if (!result.success) { - const failures = result.results?.filter((r) => !r.success) || []; - failures.forEach((f) => { - logger.error(`❌ Pre-processor ${f.processorName} failed: ${f.error}`); - }); - } - else { - result.results?.forEach((r) => { - logger.log(`✅ Pre-processor ${r.processorName}: ${r.success ? "OK" : "FAILED"}`); - }); - } - } - catch (error) { - logger.error(`💥 Pre-processor execution error`, error); - } - // PHASE 3: Execute post-processors after tool completion - try { - // Check if processorManager and method exist - if (!processorManager || typeof processorManager.executePostProcessors !== 'function') { - logger.log(`⏭️ Post-processors skipped: processor manager not available`); - return; - } - logger.log(`▶️ Executing post-processors for ${tool}...`); - logger.log(`📝 Post-processor args: ${JSON.stringify(args)}`); - const postResults = await processorManager.executePostProcessors(tool, { - directory, - operation: "tool_execution", - filePath: args?.filePath, - success: true, - }, []); - // postResults is an array of ProcessorResult - const allSuccess = postResults.every((r) => r.success); - logger.log(`📊 Post-processor result: ${allSuccess ? "SUCCESS" : "FAILED"} (${postResults.length} processors)`); - // Log each post-processor result for debugging - for (const r of postResults) { - if (r.success) { - logger.log(`✅ Post-processor ${r.processorName}: OK`); - } - else { - logger.error(`❌ Post-processor ${r.processorName} failed: ${r.error}`); - } - } - } - catch (error) { - logger.error(`💥 Post-processor execution error`, error); - } - } - }, - // Execute POST-processors AFTER tool completes (this is the correct place!) - "tool.execute.after": async (input, _output) => { - const logger = await getOrCreateLogger(directory); - const { tool, args, result } = input; - // Log tool completion to activity logger (direct write - no module isolation issues) - logToolActivity(directory, "complete", tool, args || {}, result, result?.error, result?.duration); - await loadStrRayComponents(); - // Debug: log full input - logger.log(`📥 After hook input: ${JSON.stringify({ tool, hasArgs: !!args, args, hasResult: !!result }).slice(0, 200)}`); - // Run post-processors for ALL tools AFTER tool completes - if (ProcessorManager || StrRayStateManager) { - const stateManager = new StrRayStateManager(path.join(directory, ".opencode", "state")); - const processorManager = new ProcessorManager(stateManager); - // Register post-processors - processorManager.registerProcessor({ - name: "testAutoCreation", - type: "post", - priority: 50, - enabled: true, - }); - processorManager.registerProcessor({ - name: "testExecution", - type: "post", - priority: 10, - enabled: true, - }); - processorManager.registerProcessor({ - name: "coverageAnalysis", - type: "post", - priority: 20, - enabled: true, - }); - try { - // Check if processorManager and method exist - if (!processorManager || typeof processorManager.executePostProcessors !== 'function') { - logger.log(`⏭️ Post-processors skipped: processor manager not available`); - return; - } - // Execute post-processors AFTER tool - with actual filePath for testAutoCreation - logger.log(`📝 Post-processor tool: ${tool}`); - logger.log(`📝 Post-processor args: ${JSON.stringify(args)}`); - logger.log(`📝 Post-processor directory: ${directory}`); - const postResults = await processorManager.executePostProcessors(tool, { - directory, - operation: "tool_execution", - filePath: args?.filePath, - success: result?.success !== false, - }, []); - // postResults is an array of ProcessorResult - const allSuccess = postResults.every((r) => r.success); - logger.log(`📊 Post-processor result: ${allSuccess ? "SUCCESS" : "FAILED"} (${postResults.length} processors)`); - // Log each post-processor result for debugging - for (const r of postResults) { - if (r.success) { - logger.log(`✅ Post-processor ${r.processorName}: OK`); - } - else { - logger.error(`❌ Post-processor ${r.processorName} failed: ${r.error}`); - } - } - // Log testAutoCreation results specifically - const testAutoResult = postResults.find((r) => r.processorName === "testAutoCreation"); - if (testAutoResult) { - if (testAutoResult.success && testAutoResult.testCreated) { - logger.log(`✅ TEST AUTO-CREATION: Created ${testAutoResult.testFile}`); - } - else if (!testAutoResult.success) { - logger.log(`ℹ️ TEST AUTO-CREATION: ${testAutoResult.message || "skipped - no new files"}`); - } - } - } - catch (error) { - logger.error(`💥 Post-processor error`, error); - } - } - }, - /** - * chat.message - Intercept user messages for routing - * Output contains message and parts with user content - */ - "chat.message": async (input, output) => { - const logger = await getOrCreateLogger(directory); - // DEBUG: Log ALL output - const debugLogPath = path.join(process.cwd(), "logs", "framework", "routing-debug.log"); - fs.appendFileSync(debugLogPath, `\n[${new Date().toISOString()}] === chat.message HOOK FIRED ===\n`); - fs.appendFileSync(debugLogPath, `OUTPUT KEYS: ${JSON.stringify(Object.keys(output || {}))}\n`); - fs.appendFileSync(debugLogPath, `MESSAGE: ${JSON.stringify(output?.message)}\n`); - fs.appendFileSync(debugLogPath, `PARTS: ${JSON.stringify(output?.parts)}\n`); - // Extract user message from parts (TextPart has type="text" and text field) - let userMessage = ""; - if (output?.parts && Array.isArray(output.parts)) { - for (const part of output.parts) { - if (part?.type === "text" && part?.text) { - userMessage = part.text; - break; - } - } - } - // Store original user message for tool hooks (context preservation via file) - const sessionId = output?.message?.sessionID || "default"; - const contextPath = path.join(directory, ".opencode", "logs", `context-${sessionId}.json`); - try { - fs.writeFileSync(contextPath, JSON.stringify({ - sessionId, - userMessage, - timestamp: new Date().toISOString() - }), "utf-8"); - logger.log(`💾 Context saved: ${sessionId}`); - } - catch (e) { - logger.error(`Context save failed: ${e}`); - } - globalThis.__strRayOriginalMessage = userMessage; - logger.log(`userMessage: "${userMessage.slice(0, 100)}"`); - if (!userMessage || userMessage.length === 0) { - fs.appendFileSync(debugLogPath, `SKIP: No user text found\n`); - return; - } - logger.log(`👤 User message: "${userMessage.slice(0, 50)}..."`); - try { - await loadTaskSkillRouter(); - if (taskSkillRouterInstance) { - // Get complexity score for tiebreaking - let complexityScore = 50; // default medium - try { - if (featuresConfigLoader) { - const config = featuresConfigLoader.loadConfig(); - if (config.model_routing?.complexity?.enabled) { - // Estimate complexity based on message length and keywords - complexityScore = estimateComplexity(userMessage); - } - } - } - catch (e) { - // Silent fail for complexity estimation - } - fs.appendFileSync(debugLogPath, `Complexity estimated: ${complexityScore}\n`); - // Route with complexity context - const routingResult = taskSkillRouterInstance.routeTask(userMessage, { - source: "chat_message", - complexity: complexityScore, - }); - fs.appendFileSync(debugLogPath, `Routing result: ${JSON.stringify(routingResult)}\n`); - if (routingResult && routingResult.agent) { - // Apply weighted confidence scoring - let finalConfidence = routingResult.confidence; - let routingMethod = "keyword"; - // If keyword confidence is low, use complexity-based routing - if (routingResult.confidence < 0.7 && complexityScore > 50) { - // High complexity tasks get orchestrator boost - if (complexityScore > 70) { - routingResult.agent = "orchestrator"; - finalConfidence = Math.min(0.85, routingResult.confidence + 0.15); - routingMethod = "complexity"; - } - } - // If low complexity and low confidence, boost code-reviewer - if (routingResult.confidence < 0.6 && complexityScore < 30) { - routingResult.agent = "code-reviewer"; - finalConfidence = Math.min(0.75, routingResult.confidence + 0.15); - routingMethod = "complexity"; - } - logger.log(`🎯 Routed to: @${routingResult.agent} (${Math.round(finalConfidence * 100)}%) via ${routingMethod}`); - fs.appendFileSync(debugLogPath, `Final agent: ${routingResult.agent}, confidence: ${finalConfidence}, method: ${routingMethod}\n`); - // Store routing in session for later use - const sessionRoutingPath = path.join(process.cwd(), "logs", "framework", "session-routing.json"); - try { - fs.appendFileSync(sessionRoutingPath, JSON.stringify({ - timestamp: new Date().toISOString(), - message: userMessage.slice(0, 100), - agent: routingResult.agent, - confidence: finalConfidence, - method: routingMethod, - complexity: complexityScore, - }) + "\n"); - } - catch (e) { - // Silent fail for session routing logging - } - } - } - } - catch (e) { - logger.error("Chat message routing error:", e); - fs.appendFileSync(debugLogPath, `ERROR: ${e}\n`); - } - fs.appendFileSync(debugLogPath, `=== END chat.message ===\n`); - }, - config: async (_config) => { - const logger = await getOrCreateLogger(directory); - logger.log("🔧 Plugin config hook triggered - initializing StrRay integration"); - // Initialize StrRay framework - const initScriptPath = path.join(directory, ".opencode", "init.sh"); - if (fs.existsSync(initScriptPath)) { - try { - const { stderr } = await spawnPromise("bash", [initScriptPath], directory); - if (stderr) { - logger.error(`Framework init error: ${stderr}`); - } - else { - logger.log("✅ StrRay Framework initialized successfully"); - } - } - catch (error) { - logger.error("Framework initialization failed", error); - } - } - logger.log("✅ Plugin config hook completed"); - }, - }; -} -//# sourceMappingURL=strray-codex-injection.js.map \ No newline at end of file diff --git a/src/__tests__/postprocessor/escalation/EscalationEngine.test.ts b/src/__tests__/postprocessor/escalation/EscalationEngine.test.ts index 6debea720..bf2f30beb 100644 --- a/src/__tests__/postprocessor/escalation/EscalationEngine.test.ts +++ b/src/__tests__/postprocessor/escalation/EscalationEngine.test.ts @@ -5,6 +5,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import { EscalationEngine } from "../../../postprocessor/escalation/EscalationEngine.js"; import { PostProcessorContext } from "../../../postprocessor/types.js"; +import { frameworkLogger } from "../../../core/framework-logger.js"; describe("EscalationEngine", () => { let engine: EscalationEngine; @@ -144,12 +145,19 @@ describe("EscalationEngine", () => { describe("Alerting", () => { it("should send alerts through configured channels", async () => { - const consoleSpy = vi.spyOn(console, "log"); + const loggerSpy = vi.spyOn(frameworkLogger, "log").mockResolvedValue(undefined); await engine.evaluateEscalation(mockContext, 5, "Critical error", []); - // Check that alert was sent (should contain the escalation message) - expect(consoleSpy).toHaveBeenCalled(); + // Check that alert was logged via frameworkLogger + expect(loggerSpy).toHaveBeenCalledWith( + "EscalationEngine", + "display-alert", + expect.any(String), + expect.any(Object), + ); + + loggerSpy.mockRestore(); }); }); diff --git a/src/__tests__/postprocessor/success/SuccessHandler.test.ts b/src/__tests__/postprocessor/success/SuccessHandler.test.ts index 2754d7c7e..446c806ef 100644 --- a/src/__tests__/postprocessor/success/SuccessHandler.test.ts +++ b/src/__tests__/postprocessor/success/SuccessHandler.test.ts @@ -8,6 +8,7 @@ import { PostProcessorContext, PostProcessorResult, } from "../../../postprocessor/types.js"; +import { frameworkLogger } from "../../../core/framework-logger.js"; describe("SuccessHandler", () => { let handler: SuccessHandler; @@ -105,16 +106,24 @@ describe("SuccessHandler", () => { describe("Success Confirmation", () => { it("should perform success confirmation when enabled", async () => { const customHandler = new SuccessHandler({ successConfirmation: true }); - const consoleSpy = vi.spyOn(console, "log"); + const loggerSpy = vi.spyOn(frameworkLogger, "log").mockResolvedValue(undefined); await customHandler.handleSuccess(mockContext, mockResult, []); - expect(consoleSpy).toHaveBeenCalledWith( - "🔍 Confirming deployment success...", + expect(loggerSpy).toHaveBeenCalledWith( + "SuccessHandler", + "confirm-success", + "info", + expect.objectContaining({ message: "🔍 Confirming deployment success..." }), ); - expect(consoleSpy).toHaveBeenCalledWith( - "✅ Deployment success confirmed", + expect(loggerSpy).toHaveBeenCalledWith( + "SuccessHandler", + "confirm-success", + "info", + expect.objectContaining({ message: "✅ Deployment success confirmed" }), ); + + loggerSpy.mockRestore(); }); it("should skip success confirmation when disabled", async () => { @@ -145,14 +154,18 @@ describe("SuccessHandler", () => { describe("Success Notifications", () => { it("should send notifications when enabled", async () => { const customHandler = new SuccessHandler({ notificationEnabled: true }); - const consoleSpy = vi.spyOn(console, "log"); + const loggerSpy = vi.spyOn(frameworkLogger, "log").mockResolvedValue(undefined); await customHandler.handleSuccess(mockContext, mockResult, []); - expect(consoleSpy).toHaveBeenCalledWith( - "📢 Success Notification:", - expect.any(String), + expect(loggerSpy).toHaveBeenCalledWith( + "SuccessHandler", + "send-notifications", + "info", + expect.objectContaining({ message: "📢 Success Notification:" }), ); + + loggerSpy.mockRestore(); }); it("should skip notifications when disabled", async () => { @@ -171,14 +184,24 @@ describe("SuccessHandler", () => { describe("Cleanup Operations", () => { it("should perform cleanup when enabled", async () => { const customHandler = new SuccessHandler({ cleanupEnabled: true }); - const consoleSpy = vi.spyOn(console, "log"); + const loggerSpy = vi.spyOn(frameworkLogger, "log").mockResolvedValue(undefined); await customHandler.handleSuccess(mockContext, mockResult, []); - expect(consoleSpy).toHaveBeenCalledWith( - "🧹 Performing post-success cleanup...", + expect(loggerSpy).toHaveBeenCalledWith( + "SuccessHandler", + "perform-cleanup", + "info", + expect.objectContaining({ message: "🧹 Performing post-success cleanup..." }), + ); + expect(loggerSpy).toHaveBeenCalledWith( + "SuccessHandler", + "perform-cleanup", + "info", + expect.objectContaining({ message: "✅ Cleanup completed" }), ); - expect(consoleSpy).toHaveBeenCalledWith("✅ Cleanup completed"); + + loggerSpy.mockRestore(); }); it("should skip cleanup when disabled", async () => { @@ -196,14 +219,24 @@ describe("SuccessHandler", () => { describe("Metrics Collection", () => { it("should collect and log metrics when enabled", async () => { const customHandler = new SuccessHandler({ metricsCollection: true }); - const consoleSpy = vi.spyOn(console, "log"); + const loggerSpy = vi.spyOn(frameworkLogger, "log").mockResolvedValue(undefined); await customHandler.handleSuccess(mockContext, mockResult, []); - expect(consoleSpy).toHaveBeenCalledWith("📊 Success Metrics:"); - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining("Total Duration:"), + expect(loggerSpy).toHaveBeenCalledWith( + "SuccessHandler", + "log-metrics", + "info", + expect.objectContaining({ message: "📊 Success Metrics:" }), + ); + expect(loggerSpy).toHaveBeenCalledWith( + "SuccessHandler", + "log-metrics", + "info", + expect.objectContaining({ message: expect.stringContaining("Total Duration:") }), ); + + loggerSpy.mockRestore(); }); it("should skip metrics collection when disabled", async () => { diff --git a/src/__tests__/unit/security/security-headers.test.ts b/src/__tests__/unit/security/security-headers.test.ts index fe4709d5c..9e30704a1 100644 --- a/src/__tests__/unit/security/security-headers.test.ts +++ b/src/__tests__/unit/security/security-headers.test.ts @@ -13,6 +13,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import { SecurityHeadersMiddleware } from "../../../security/security-headers.js"; +import { frameworkLogger } from "../../../core/framework-logger.js"; describe("SecurityHeadersMiddleware", () => { let middleware: SecurityHeadersMiddleware; @@ -62,15 +63,18 @@ describe("SecurityHeadersMiddleware", () => { }); it("should handle invalid response object gracefully", () => { - const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const loggerSpy = vi.spyOn(frameworkLogger, "log").mockResolvedValue(undefined); middleware.applySecurityHeaders(null as any); middleware.applySecurityHeaders({} as any); - expect(consoleSpy).toHaveBeenCalledWith( - "SecurityHeadersMiddleware: Invalid response object", + expect(loggerSpy).toHaveBeenCalledWith( + "security-headers", + "invalid-response-object", + "warning", + expect.objectContaining({ warning: "SecurityHeadersMiddleware: Invalid response object" }), ); - consoleSpy.mockRestore(); + loggerSpy.mockRestore(); }); it("should apply custom CSP when configured", () => { diff --git a/src/core/config-paths.ts b/src/core/config-paths.ts index 84b4785fe..121619175 100644 --- a/src/core/config-paths.ts +++ b/src/core/config-paths.ts @@ -94,7 +94,7 @@ export function resolveConfigPath(relativePath: string, projectRoot?: string): s } /** - * Get the state persistence directory. + * Get the state persistence directory (the parent directory for state.json). * Similar logic to resolveConfigPath but for the state/ subdirectory. */ export function resolveStateDir(projectRoot?: string): string { @@ -118,6 +118,14 @@ export function resolveStateDir(projectRoot?: string): string { return candidates[0]!; } +/** + * Get the state persistence FILE path (e.g. .opencode/state/state.json). + * Prefer this over resolveStateDir when you need a file path for fs.writeFileSync. + */ +export function resolveStateFilePath(projectRoot?: string): string { + return join(resolveStateDir(projectRoot), "state.json"); +} + /** * Get the profiles storage directory. */ diff --git a/src/core/framework-logger.ts b/src/core/framework-logger.ts index 6909987a7..b9f0da7d3 100644 --- a/src/core/framework-logger.ts +++ b/src/core/framework-logger.ts @@ -36,19 +36,19 @@ export function withJobContext( if (result instanceof Promise) { return result.finally(async () => { // Auto-complete job on operation finish - await jobContext.complete(true).catch(console.error); + await jobContext.complete(true).catch(() => {}); // Restore original context currentJobContext = originalContext; }); } else { // Sync operation - complete immediately - jobContext.complete(true).catch(console.error); + jobContext.complete(true).catch(() => {}); currentJobContext = originalContext; return Promise.resolve(result); } } catch (error) { // Error occurred - complete job with failure - jobContext.complete(false, { error: String(error) }).catch(console.error); + jobContext.complete(false, { error: String(error) }).catch(() => {}); currentJobContext = originalContext; throw error; } diff --git a/src/enforcement/enforcer-tools.ts b/src/enforcement/enforcer-tools.ts index 28624eafc..423c84277 100644 --- a/src/enforcement/enforcer-tools.ts +++ b/src/enforcement/enforcer-tools.ts @@ -337,7 +337,7 @@ async function ruleValidationSelf( return { operation, passed: report.passed, - blocked: !report.passed && report.errors.some(e => e.includes("required") || e.includes("violation")), + blocked: report.errors.length > 0, errors: report.errors, warnings: report.warnings, fixes: [], @@ -476,11 +476,7 @@ export async function ruleValidation( const result: EnforcementResult = { operation, passed: report.passed, - blocked: - !report.passed && - report.errors.some( - (e) => e.includes("required") || e.includes("violation"), - ), + blocked: report.errors.length > 0, errors: report.errors, warnings: report.warnings, fixes: [], @@ -597,7 +593,7 @@ export async function contextAnalysisValidation( ...validationResult, errors: [...validationResult.errors, ...contextIssues.errors], warnings: [...validationResult.warnings, ...contextIssues.warnings], - blocked: validationResult.blocked || contextIssues.errors.length > 0, + blocked: validationResult.blocked, }; await frameworkLogger.log( @@ -662,10 +658,14 @@ export async function codexEnforcement( // Generate codex compliance report const codexReport = await generateCodexComplianceReport(files, newCode); + const combinedErrors = [...validationResult.errors, ...codexReport.violations]; + const combinedWarnings = [...validationResult.warnings, ...codexReport.warnings]; + const result: EnforcementResult = { ...validationResult, - errors: [...validationResult.errors, ...codexReport.violations], - warnings: [...validationResult.warnings, ...codexReport.warnings], + errors: combinedErrors, + warnings: combinedWarnings, + blocked: combinedErrors.length > 0, }; await frameworkLogger.log( diff --git a/src/integrations/base/Integration.ts b/src/integrations/base/Integration.ts index 7e0380e90..7fc8c409a 100644 --- a/src/integrations/base/Integration.ts +++ b/src/integrations/base/Integration.ts @@ -404,8 +404,7 @@ export abstract class BaseIntegration this.jobId, ); } catch { - // Fallback to console if logger fails - console.log(`[${this.name}] ${message}`, details || ""); + // Silent fail - logging should never break application } } diff --git a/src/integrations/base/registry.ts b/src/integrations/base/registry.ts index 5f7088f6a..242b307ce 100644 --- a/src/integrations/base/registry.ts +++ b/src/integrations/base/registry.ts @@ -174,7 +174,7 @@ export class IntegrationRegistry extends EventEmitter { "info", { name, version: integration.version }, this.jobId, - ).catch(console.error); + ).catch(() => {}); } /** @@ -199,7 +199,7 @@ export class IntegrationRegistry extends EventEmitter { "error", { name, error: String(error) }, this.jobId, - ).catch(console.error); + ).catch(() => {}); }); } @@ -213,7 +213,7 @@ export class IntegrationRegistry extends EventEmitter { "info", { name }, this.jobId, - ).catch(console.error); + ).catch(() => {}); } // ========================================================================== @@ -298,7 +298,7 @@ export class IntegrationRegistry extends EventEmitter { "warning", { name }, this.jobId, - ).catch(console.error); + ).catch(() => {}); return; } @@ -338,7 +338,7 @@ export class IntegrationRegistry extends EventEmitter { "success", { name, version: integration.version }, this.jobId, - ).catch(console.error); + ).catch(() => {}); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -384,7 +384,7 @@ export class IntegrationRegistry extends EventEmitter { "success", { name }, this.jobId, - ).catch(console.error); + ).catch(() => {}); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); @@ -403,7 +403,7 @@ export class IntegrationRegistry extends EventEmitter { "error", { name, error: errorMessage }, this.jobId, - ).catch(console.error); + ).catch(() => {}); } } @@ -422,7 +422,7 @@ export class IntegrationRegistry extends EventEmitter { "info", { total: entries.length, enabled: enabledEntries.length }, this.jobId, - ).catch(console.error); + ).catch(() => {}); const results: Array<{ name: string; success: boolean; error?: string }> = []; @@ -437,7 +437,7 @@ export class IntegrationRegistry extends EventEmitter { "warning", { name }, this.jobId, - ).catch(console.error); + ).catch(() => {}); results.push({ name, success: false, error: "Not registered" }); continue; } @@ -459,7 +459,7 @@ export class IntegrationRegistry extends EventEmitter { "error", { name, error: errorMessage }, this.jobId, - ).catch(console.error); + ).catch(() => {}); } } @@ -479,7 +479,7 @@ export class IntegrationRegistry extends EventEmitter { failed > 0 ? "warning" : "success", { succeeded, failed }, this.jobId, - ).catch(console.error); + ).catch(() => {}); } /** @@ -494,7 +494,7 @@ export class IntegrationRegistry extends EventEmitter { "info", { count: loadedNames.length }, this.jobId, - ).catch(console.error); + ).catch(() => {}); const results: Array<{ name: string; success: boolean; error?: string }> = []; @@ -525,7 +525,7 @@ export class IntegrationRegistry extends EventEmitter { failed > 0 ? "warning" : "success", { succeeded, failed }, this.jobId, - ).catch(console.error); + ).catch(() => {}); } // ========================================================================== @@ -658,7 +658,7 @@ export class IntegrationRegistry extends EventEmitter { "info", { cleared: names.length - this.loadedIntegrations.size }, this.jobId, - ).catch(console.error); + ).catch(() => {}); } /** @@ -762,7 +762,7 @@ export async function discoverIntegrations( `Integrations directory not found: ${integrationsPath}`, "warning", { path: integrationsPath }, - ).catch(console.error); + ).catch(() => {}); return discovered; } @@ -815,7 +815,7 @@ export async function discoverIntegrations( `Discovered integration: ${name}`, "info", { name, path: indexPath }, - ).catch(console.error); + ).catch(() => {}); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); frameworkLogger.log( @@ -823,7 +823,7 @@ export async function discoverIntegrations( `Failed to load integration from ${indexPath}: ${errorMessage}`, "warning", { path: indexPath, error: errorMessage }, - ).catch(console.error); + ).catch(() => {}); } } } @@ -834,7 +834,7 @@ export async function discoverIntegrations( `Error during integration discovery: ${errorMessage}`, "error", { error: errorMessage }, - ).catch(console.error); + ).catch(() => {}); } return discovered; @@ -898,7 +898,7 @@ export async function autoRegisterIntegrations( `Failed to register integration '${d.name}': ${errorMessage}`, "warning", { name: d.name, error: errorMessage }, - ).catch(console.error); + ).catch(() => {}); } } diff --git a/src/integrations/hermes-agent/__init__.py b/src/integrations/hermes-agent/__init__.py index 85e94a03e..93f38892e 100644 --- a/src/integrations/hermes-agent/__init__.py +++ b/src/integrations/hermes-agent/__init__.py @@ -113,6 +113,9 @@ def _find_project_root(): "post_processor_runs": 0, "bridge_calls": 0, "bridge_errors": 0, + "subagent_dispatches": 0, + "subagent_validations": 0, + "subagent_blocks": 0, } # ── File logging ────────────────────────────────────────────── @@ -202,6 +205,16 @@ def _on_pre_tool_call(tool_name: str, args: dict, task_id: str, **kwargs): _log_to_file("activity.log", f"[quality-gate] SKIP (strray-mcp): {tool_name}") return + # delegate_task: snapshot working tree so post_hook can validate changes + if tool_name == "delegate_task": + tid = kwargs.get("task_id", "") or args.get("task_id", "") or task_id + if tid: + _delegate_snapshots[tid] = _snapshot_working_tree() + _session_stats["subagent_dispatches"] += 1 + _log_to_file("activity.log", + f"[pre-tool] SUBAGENT DISPATCH: task_id={tid}") + return + _session_stats["native_tool_calls"] += 1 # Code-producing tools get the full pipeline @@ -311,6 +324,13 @@ def _on_post_tool_call(tool_name: str, args: dict, result, task_id: str, **kwarg error = result["error"] _log_tool_event("complete", tool_name, args, duration, error) + # delegate_task: validate all files the subagent changed + if tool_name == "delegate_task": + tid = kwargs.get("task_id", "") or args.get("task_id", "") or task_id + if tid: + _validate_subagent_changes(tid) + return + # Track file modifications if file_path: _log_to_file("activity.log", @@ -355,7 +375,8 @@ def _on_session_start(session_id: str, platform: str, **kwargs): for key in ("code_operations", "total_tool_calls", "strray_mcp_calls", "native_tool_calls", "quality_gate_runs", "quality_gate_blocks", "pre_processor_runs", "post_processor_runs", - "bridge_calls", "bridge_errors"): + "bridge_calls", "bridge_errors", + "subagent_dispatches", "subagent_validations", "subagent_blocks"): _session_stats[key] = 0 _ensure_log_dir() @@ -384,7 +405,10 @@ def _strray_command(args: str) -> str: f" Pre-processor runs: {_session_stats['pre_processor_runs']}\n" f" Post-processor runs: {_session_stats['post_processor_runs']}\n" f" Bridge calls: {_session_stats['bridge_calls']}\n" - f" Bridge errors: {_session_stats['bridge_errors']}" + f" Bridge errors: {_session_stats['bridge_errors']}\n" + f" Subagent dispatches: {_session_stats['subagent_dispatches']}\n" + f" Subagent validations: {_session_stats['subagent_validations']}\n" + f" Subagent blocks: {_session_stats['subagent_blocks']}" ) if cmd == "help": @@ -413,11 +437,106 @@ def _strray_command(args: str) -> str: # ── Registration ────────────────────────────────────────────── -# Session tracking for new lifecycle hooks +# ── Session tracking for new lifecycle hooks _modified_files: list = [] _validation_results: list = [] _errors: list = [] +# ── Subagent (delegate_task) enforcement ──────────────────── +# Subagents bypass all StringRay hooks because they run in isolated +# contexts. We enforce by snapshotting the working tree before dispatch +# and validating all changed files after return. + +_delegate_snapshots: dict = {} # task_id → set of (path, mtime) + + +def _snapshot_working_tree() -> dict: + """Snapshot file mtimes under project root for subagent change detection.""" + try: + result = subprocess.run( + ["git", "-C", str(PROJECT_ROOT), "diff", "--name-only", "HEAD"], + capture_output=True, text=True, timeout=5, + ) + if result.returncode == 0: + changed = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set() + else: + changed = set() + except (FileNotFoundError, subprocess.TimeoutExpired): + changed = set() + return {"changed_before": changed} + + +def _validate_subagent_changes(task_id: str, **kwargs): + """After delegate_task returns, find what the subagent changed and validate.""" + snapshot = _delegate_snapshots.pop(task_id, None) + if not snapshot: + return + + before = snapshot.get("changed_before", set()) + + try: + result = subprocess.run( + ["git", "-C", str(PROJECT_ROOT), "diff", "--name-only", "HEAD"], + capture_output=True, text=True, timeout=5, + ) + if result.returncode != 0: + return + after = set(result.stdout.strip().split("\n")) if result.stdout.strip() else set() + except (FileNotFoundError, subprocess.TimeoutExpired): + return + + new_changes = after - before + if not new_changes: + return + + # Filter to source files only (skip dist, node_modules, logs, etc.) + source_files = sorted(f for f in new_changes if any( + f.startswith(prefix) for prefix in ("src/", "dist/", "scripts/", ".opencode/plugins/") + ) and not any( + skip in f for skip in ("node_modules/", ".log", "__pycache__", ".map") + )) + + if not source_files: + return + + _session_stats["code_operations"] += len(source_files) + _session_stats["subagent_validations"] += 1 + + # Resolve to absolute paths for validation + abs_files = [str(PROJECT_ROOT / f) for f in source_files] + + # Run validation on all changed files via bridge + bridge_result = _call_bridge({ + "command": "validate", + "files": abs_files, + "operation": "modify", + }, timeout=30) + + if "error" in bridge_result: + _log_to_file("activity.log", + f"[subagent-validate] BRIDGE ERROR: {bridge_result['error']}") + return + + results = bridge_result.get("fileResults", bridge_result.get("results", {})) + for rel_path in source_files: + file_result = results.get(rel_path, results.get(str(PROJECT_ROOT / rel_path), {})) + passed = file_result.get("passed", True) + violations = file_result.get("violations", []) + + if not passed: + _session_stats["quality_gate_blocks"] += 1 + _session_stats["subagent_blocks"] += 1 + _log_to_file("activity.log", + f"[subagent-validate] BLOCKED: {rel_path} " + f"violations={'; '.join(str(v) for v in violations[:3])}") + logger.warning( + "[strray] Subagent BLOCKED %s: %s", + rel_path, violations[:3], + ) + else: + _log_to_file("activity.log", + f"[subagent-validate] PASSED: {rel_path}") + def _on_file_write(file_path: str, content: str, tool_name: str, **kwargs): """Fires when a code-producing tool writes a file. @@ -533,11 +652,11 @@ def register(ctx): # ── Bootstrap ───────────────────────────────────────────── _ensure_log_dir() _log_to_file("activity.log", - f"[plugin-loaded] StringRay Hermes Plugin v2.1 — " - f"4 tools, 5 hooks, bridge={BRIDGE_PATH.exists()}") + f"[plugin-loaded] StringRay Hermes Plugin v2.2 — " + f"4 tools, 5 hooks, subagent enforcement, bridge={BRIDGE_PATH.exists()}") logger.info( - "[strray] Plugin v2.1 loaded: 4 tools, 5 hooks, " - "bridge=%s — full framework pipeline active", + "[strray] Plugin v2.2 loaded: 4 tools, 5 hooks, " + "subagent enforcement active, bridge=%s", BRIDGE_PATH.exists(), ) diff --git a/src/jobs/job-correlation-fix.ts b/src/jobs/job-correlation-fix.ts index 545bec8e7..c204a99b1 100644 --- a/src/jobs/job-correlation-fix.ts +++ b/src/jobs/job-correlation-fix.ts @@ -21,7 +21,7 @@ export function withJobContext(operation: () => T, jobId?: string): T { return operation(); } finally { // Auto-complete job on operation finish - jobContext.complete(true).catch(console.error); + jobContext.complete(true).catch(() => {}); } } diff --git a/src/mcps/connection/connection-manager.ts b/src/mcps/connection/connection-manager.ts index 4e0667890..65ebfb2eb 100644 --- a/src/mcps/connection/connection-manager.ts +++ b/src/mcps/connection/connection-manager.ts @@ -1,5 +1,6 @@ import { IMcpConnection, IServerConfig } from '../types/index.js'; import { McpConnection } from './mcp-connection.js'; +import { frameworkLogger } from '../../core/framework-logger.js'; /** * Connection Manager @@ -52,7 +53,7 @@ export class ConnectionManager { for (const [serverName, connection] of this.connections) { disconnectPromises.push( connection.disconnect().catch((error) => { - console.error(`Error disconnecting ${serverName}:`, error); + frameworkLogger.log("connection-manager", "disconnect-error", "error", { serverName, error }); }) ); } diff --git a/src/mcps/connection/connection-pool.ts b/src/mcps/connection/connection-pool.ts index 56f1ff98e..6ee7d3038 100644 --- a/src/mcps/connection/connection-pool.ts +++ b/src/mcps/connection/connection-pool.ts @@ -1,5 +1,6 @@ import { IMcpConnection, IServerConfig } from '../types/index.js'; import { McpConnection } from './mcp-connection.js'; +import { frameworkLogger } from '../../core/framework-logger.js'; /** * Pooled Connection @@ -106,7 +107,7 @@ export class ConnectionPool implements IConnectionPoolExtended { for (const pooled of pool) { disconnectPromises.push( pooled.connection.disconnect().catch((error) => { - console.error('Error disconnecting pooled connection:', error); + frameworkLogger.log("connection-pool", "disconnect-error", "error", { error }); }) ); } @@ -193,7 +194,7 @@ export class ConnectionPool implements IConnectionPoolExtended { ) { // Disconnect and remove stale connection pooled.connection.disconnect().catch((error) => { - console.error('Error disconnecting stale connection:', error); + frameworkLogger.log("connection-pool", "stale-disconnect-error", "error", { error }); }); pool.splice(i, 1); } diff --git a/src/mcps/knowledge-skills/api-design.server.ts b/src/mcps/knowledge-skills/api-design.server.ts index 7615d2f1f..d6d79541a 100644 --- a/src/mcps/knowledge-skills/api-design.server.ts +++ b/src/mcps/knowledge-skills/api-design.server.ts @@ -143,7 +143,7 @@ class StrRayApiDesignServer { if (import.meta.url === `file://${process.argv[1]}`) { const server = new StrRayApiDesignServer(); - server.run().catch(console.error); + server.run().catch(() => {}); } export default StrRayApiDesignServer; diff --git a/src/mcps/knowledge-skills/architecture-patterns.server.ts b/src/mcps/knowledge-skills/architecture-patterns.server.ts index 251f0f6a5..d4f5996dd 100644 --- a/src/mcps/knowledge-skills/architecture-patterns.server.ts +++ b/src/mcps/knowledge-skills/architecture-patterns.server.ts @@ -135,7 +135,7 @@ class StrRayArchitecturePatternsServer { if (import.meta.url === `file://${process.argv[1]}`) { const server = new StrRayArchitecturePatternsServer(); - server.run().catch(console.error); + server.run().catch(() => {}); } export default StrRayArchitecturePatternsServer; diff --git a/src/mcps/knowledge-skills/code-analyzer.server.ts b/src/mcps/knowledge-skills/code-analyzer.server.ts index 01571130d..726c80a9d 100644 --- a/src/mcps/knowledge-skills/code-analyzer.server.ts +++ b/src/mcps/knowledge-skills/code-analyzer.server.ts @@ -550,5 +550,5 @@ class CodeAnalyzerServer { } } -if (import.meta.url === `file://${process.argv[1]}`) { new CodeAnalyzerServer().run().catch(console.error); } +if (import.meta.url === `file://${process.argv[1]}`) { new CodeAnalyzerServer().run().catch(() => {}); } export default CodeAnalyzerServer; diff --git a/src/mcps/knowledge-skills/code-review.server.ts b/src/mcps/knowledge-skills/code-review.server.ts index 7eaa32803..300dd1a62 100644 --- a/src/mcps/knowledge-skills/code-review.server.ts +++ b/src/mcps/knowledge-skills/code-review.server.ts @@ -1026,7 +1026,7 @@ class StrRayCodeReviewServer { // Run the server if this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { const server = new StrRayCodeReviewServer(); - server.run().catch(console.error); + server.run().catch(() => {}); } export { StrRayCodeReviewServer }; diff --git a/src/mcps/knowledge-skills/database-design.server.ts b/src/mcps/knowledge-skills/database-design.server.ts index 9807f880f..20881eec0 100644 --- a/src/mcps/knowledge-skills/database-design.server.ts +++ b/src/mcps/knowledge-skills/database-design.server.ts @@ -1156,7 +1156,7 @@ class StrRayDatabaseDesignServer { // Run the server if this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { const server = new StrRayDatabaseDesignServer(); - server.run().catch(console.error); + server.run().catch(() => {}); } export { StrRayDatabaseDesignServer }; diff --git a/src/mcps/knowledge-skills/devops-deployment.server.ts b/src/mcps/knowledge-skills/devops-deployment.server.ts index 0082d96e4..10de9bdaf 100644 --- a/src/mcps/knowledge-skills/devops-deployment.server.ts +++ b/src/mcps/knowledge-skills/devops-deployment.server.ts @@ -1479,7 +1479,7 @@ spec: // Run the server if this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { const server = new StrRayDevOpsDeploymentServer(); - server.run().catch(console.error); + server.run().catch(() => {}); } export { StrRayDevOpsDeploymentServer }; diff --git a/src/mcps/knowledge-skills/git-workflow.server.ts b/src/mcps/knowledge-skills/git-workflow.server.ts index 01a70fc98..9850c926c 100644 --- a/src/mcps/knowledge-skills/git-workflow.server.ts +++ b/src/mcps/knowledge-skills/git-workflow.server.ts @@ -134,7 +134,7 @@ class StrRayGitWorkflowServer { if (import.meta.url === `file://${process.argv[1]}`) { const server = new StrRayGitWorkflowServer(); - server.run().catch(console.error); + server.run().catch(() => {}); } export default StrRayGitWorkflowServer; diff --git a/src/mcps/knowledge-skills/mobile-development.server.ts b/src/mcps/knowledge-skills/mobile-development.server.ts index 8d55dee95..808cb6f5a 100644 --- a/src/mcps/knowledge-skills/mobile-development.server.ts +++ b/src/mcps/knowledge-skills/mobile-development.server.ts @@ -635,4 +635,4 @@ class HomePage extends StatelessWidget { // Start server if run directly const server = new StrRayMobileDevelopmentServer(); -server.start().catch(console.error); +server.start().catch(() => {}); diff --git a/src/mcps/knowledge-skills/performance-optimization.server.ts b/src/mcps/knowledge-skills/performance-optimization.server.ts index b32458f49..9e09880ca 100644 --- a/src/mcps/knowledge-skills/performance-optimization.server.ts +++ b/src/mcps/knowledge-skills/performance-optimization.server.ts @@ -129,7 +129,7 @@ class StrRayPerformanceOptimizationServer { if (import.meta.url === `file://${process.argv[1]}`) { const server = new StrRayPerformanceOptimizationServer(); - server.run().catch(console.error); + server.run().catch(() => {}); } export default StrRayPerformanceOptimizationServer; diff --git a/src/mcps/knowledge-skills/refactoring-strategies.server.ts b/src/mcps/knowledge-skills/refactoring-strategies.server.ts index 110d728c8..c200c986d 100644 --- a/src/mcps/knowledge-skills/refactoring-strategies.server.ts +++ b/src/mcps/knowledge-skills/refactoring-strategies.server.ts @@ -986,7 +986,7 @@ class StrRayRefactoringStrategiesServer { // Run the server if this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { const server = new StrRayRefactoringStrategiesServer(); - server.run().catch(console.error); + server.run().catch(() => {}); } export { StrRayRefactoringStrategiesServer }; diff --git a/src/mcps/knowledge-skills/security-audit.server.ts b/src/mcps/knowledge-skills/security-audit.server.ts index 2a0f90086..19def49d3 100644 --- a/src/mcps/knowledge-skills/security-audit.server.ts +++ b/src/mcps/knowledge-skills/security-audit.server.ts @@ -1087,7 +1087,7 @@ class StrRaySecurityAuditServer { // Run the server if this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { const server = new StrRaySecurityAuditServer(); - server.run().catch(console.error); + server.run().catch(() => {}); } export { StrRaySecurityAuditServer }; diff --git a/src/mcps/knowledge-skills/skill-invocation.server.ts b/src/mcps/knowledge-skills/skill-invocation.server.ts index e3950909b..a1871212b 100644 --- a/src/mcps/knowledge-skills/skill-invocation.server.ts +++ b/src/mcps/knowledge-skills/skill-invocation.server.ts @@ -570,7 +570,7 @@ class SkillInvocationServer { // Start the server if this file is run directly if (import.meta.url === `file://${process.argv[1]}`) { const server = new SkillInvocationServer(); - server.run().catch(console.error); + server.run().catch(() => {}); } export { SkillInvocationServer }; diff --git a/src/mcps/knowledge-skills/tech-writer.server.ts b/src/mcps/knowledge-skills/tech-writer.server.ts index 6a25302f4..a9b33e8f6 100644 --- a/src/mcps/knowledge-skills/tech-writer.server.ts +++ b/src/mcps/knowledge-skills/tech-writer.server.ts @@ -1586,7 +1586,7 @@ class StrRayDocumentationGenerationServer { // Run the server if this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { const server = new StrRayDocumentationGenerationServer(); - server.run().catch(console.error); + server.run().catch(() => {}); } export { StrRayDocumentationGenerationServer }; diff --git a/src/mcps/knowledge-skills/testing-best-practices.server.ts b/src/mcps/knowledge-skills/testing-best-practices.server.ts index 174643309..2cd9a7b03 100644 --- a/src/mcps/knowledge-skills/testing-best-practices.server.ts +++ b/src/mcps/knowledge-skills/testing-best-practices.server.ts @@ -1078,7 +1078,7 @@ class StrRayTestingBestPracticesServer { // Run the server if this file is executed directly if (import.meta.url === `file://${process.argv[1]}`) { const server = new StrRayTestingBestPracticesServer(); - server.run().catch(console.error); + server.run().catch(() => {}); } export { StrRayTestingBestPracticesServer }; diff --git a/src/mcps/knowledge-skills/testing-strategy.server.ts b/src/mcps/knowledge-skills/testing-strategy.server.ts index efcc83594..182a4943a 100644 --- a/src/mcps/knowledge-skills/testing-strategy.server.ts +++ b/src/mcps/knowledge-skills/testing-strategy.server.ts @@ -1077,7 +1077,7 @@ describe("${pathModule.basename(sourceFile, ".ts")}", () => {${testCases} // Start the server if run directly if (import.meta.url === `file://${process.argv[1]}`) { const server = new StrRayTestingStrategyServer(); - server.run().catch(console.error); + server.run().catch(() => {}); } export default StrRayTestingStrategyServer; diff --git a/src/performance/performance-budget-enforcer.ts b/src/performance/performance-budget-enforcer.ts index 022911b1d..95e8d9d0f 100644 --- a/src/performance/performance-budget-enforcer.ts +++ b/src/performance/performance-budget-enforcer.ts @@ -18,6 +18,7 @@ import * as fs from "fs"; import * as path from "path"; import { execSync } from "child_process"; import { performance } from "perf_hooks"; +import { frameworkLogger } from "../core/framework-logger.js"; // Performance budget constants from Universal Development Codex export const PERFORMANCE_BUDGET = { @@ -157,7 +158,7 @@ export class PerformanceBudgetEnforcer extends EventEmitter { }); gzippedSize = parseInt(gzipped.trim()); } catch (error) { - console.warn(`Failed to gzip ${file}:`, error); + frameworkLogger.log("performance-budget-enforcer", "gzip-failed", "warning", { file, error }); gzippedSize = size; // Fallback to uncompressed size } @@ -466,27 +467,29 @@ export class PerformanceBudgetEnforcer extends EventEmitter { */ private handleViolation(violation: PerformanceBudgetViolation): void { const severity = violation.severity.toUpperCase(); - console.warn( - `[${severity}] Performance budget violation: ${violation.metric}`, - ); - console.warn( - ` Actual: ${violation.actual.toLocaleString()}, Budget: ${violation.budget.toLocaleString()} (${violation.percentage.toFixed(1)}%)`, - ); - console.warn(` Recommendation: ${violation.recommendation}`); + frameworkLogger.log("performance-budget-enforcer", "budget-violation", "warning", { + severity, + metric: violation.metric, + actual: violation.actual.toLocaleString(), + budget: violation.budget.toLocaleString(), + percentage: violation.percentage.toFixed(1) + "%", + recommendation: violation.recommendation, + }); } /** * Handle budget exceeded events */ private handleBudgetExceeded(violation: PerformanceBudgetViolation): void { - console.error( - `🚨 PERFORMANCE BUDGET EXCEEDED: ${violation.metric} (${violation.percentage.toFixed(1)}% of budget)`, - ); + frameworkLogger.log("performance-budget-enforcer", "budget-exceeded", "error", { + metric: violation.metric, + percentage: violation.percentage.toFixed(1) + "% of budget", + }); if (violation.severity === "critical") { - console.error( - "🚨 CRITICAL: This violation requires immediate attention before deployment", - ); + frameworkLogger.log("performance-budget-enforcer", "critical-violation", "error", { + message: "CRITICAL: This violation requires immediate attention before deployment", + }); } } @@ -507,7 +510,7 @@ export class PerformanceBudgetEnforcer extends EventEmitter { try { await this.generatePerformanceReport(); } catch (error) { - console.error("Performance monitoring error:", error); + frameworkLogger.log("performance-budget-enforcer", "monitoring-error", "error", { error }); } setTimeout(monitor, intervalMs); diff --git a/src/performance/performance-ci-gates.ts b/src/performance/performance-ci-gates.ts index 6453ee20f..7168618ea 100644 --- a/src/performance/performance-ci-gates.ts +++ b/src/performance/performance-ci-gates.ts @@ -192,7 +192,7 @@ export class PerformanceCIGates { result.regressionCheck.baselineUpdated = true; } } catch (error) { - console.error("❌ Performance gates failed with error:", error); + frameworkLogger.log("performance-ci-gates", "gates-failed", "error", { error }); result.success = false; } @@ -587,12 +587,11 @@ jobs: import('./performance/performance-ci-gates.js').then(({ performanceCIGates }) => { return performanceCIGates.runPerformanceGates().then(result => { if (!result.success) { - console.error('Performance gates failed'); process.exit(1); } await frameworkLogger.log('performance-ci-gates', '-performance-gates-passed-', 'info', { message: 'Performance gates passed' }); }); - }).catch(console.error); + }).catch(() => {}); " - name: Upload performance reports @@ -688,13 +687,11 @@ pipeline { performanceCIGates.runPerformanceGates().then(result => { return performanceCIGates.runPerformanceGates().then(result => { if (!result.success) { - console.error('Performance gates failed'); process.exit(1); } await frameworkLogger.log('performance-ci-gates', '-performance-gates-passed-', 'info', { message: 'Performance gates passed' }); }); - }).catch(err => { - console.error(err); + }).catch(() => { process.exit(1); }); " @@ -749,12 +746,11 @@ steps: const { performanceCIGates } = await importResolver.importModule('performance/performance-ci-gates'); return performanceCIGates.runPerformanceGates().then(result => { if (!result.success) { - console.error('Performance gates failed'); process.exit(1); } await frameworkLogger.log('performance-ci-gates', '-performance-gates-passed-', 'info', { message: 'Performance gates passed' }); }); - })().catch(console.error); + })().catch(() => {}); " displayName: 'Run Performance Gates' diff --git a/src/performance/performance-monitoring-dashboard.ts b/src/performance/performance-monitoring-dashboard.ts index ce502aa5c..8e736bf92 100644 --- a/src/performance/performance-monitoring-dashboard.ts +++ b/src/performance/performance-monitoring-dashboard.ts @@ -236,7 +236,7 @@ export class PerformanceMonitoringDashboard extends EventEmitter { this.metrics.timestamp = timestamp; this.emit("metrics-updated", this.metrics); } catch (error) { - console.error("Failed to update dashboard metrics:", error); + frameworkLogger.log("performance-dashboard", "metrics-update-failed", "error", { error }); this.emit("error", error); } } @@ -265,7 +265,7 @@ export class PerformanceMonitoringDashboard extends EventEmitter { (h) => timestamp - h.timestamp < retentionMs, ); } catch (error) { - console.warn("Failed to update bundle size metrics:", error); + frameworkLogger.log("performance-dashboard", "bundle-size-update-failed", "warning", { error }); } } @@ -322,7 +322,7 @@ export class PerformanceMonitoringDashboard extends EventEmitter { (h) => timestamp - h.timestamp < retentionMs, ); } catch (error) { - console.warn("Failed to update web vitals metrics:", error); + frameworkLogger.log("performance-dashboard", "web-vitals-update-failed", "warning", { error }); } } @@ -667,7 +667,7 @@ export class PerformanceMonitoringDashboard extends EventEmitter { { jobId, message: alert.message }, ); } catch (error) { - console.error("Failed to send webhook notification:", error); + frameworkLogger.log("performance-dashboard", "webhook-failed", "error", { error }); } } diff --git a/src/performance/performance-regression-tester.ts b/src/performance/performance-regression-tester.ts index 083b7310e..88a40f5f2 100644 --- a/src/performance/performance-regression-tester.ts +++ b/src/performance/performance-regression-tester.ts @@ -12,6 +12,7 @@ import { performance } from "perf_hooks"; import * as fs from "fs"; import * as path from "path"; import { execSync } from "child_process"; +import { frameworkLogger } from "../core/framework-logger.js"; import { PerformanceBudgetEnforcer, PerformanceReport, @@ -81,7 +82,7 @@ export class PerformanceRegressionTester { // No existing baselines found, will use defaults } } catch (error) { - console.error(`Failed to load baselines from ${baselineFile}:`, error); + frameworkLogger.log("performance-regression-tester", "baseline-load-failed", "error", { file: baselineFile, error }); } } @@ -93,7 +94,7 @@ export class PerformanceRegressionTester { const baselines = Object.fromEntries(this.baselines); fs.writeFileSync(baselineFile, JSON.stringify(baselines, null, 2)); } catch (error) { - console.error(`Failed to save baselines to ${baselineFile}:`, error); + frameworkLogger.log("performance-regression-tester", "baseline-save-failed", "error", { file: baselineFile, error }); } } @@ -213,7 +214,7 @@ export class PerformanceRegressionTester { : "N/A"; if (result.error) { - console.error(`Test ${result.testName} failed:`, result.error); + frameworkLogger.log("performance-regression-tester", "test-failed", "error", { testName: result.testName, error: result.error }); } } @@ -225,7 +226,7 @@ export class PerformanceRegressionTester { try { budgetReport = await this.budgetEnforcer.generatePerformanceReport(); } catch (error) { - console.error("Failed to generate performance budget report:", error); + frameworkLogger.log("performance-regression-tester", "budget-report-failed", "error", { error }); } const suiteDuration = performance.now() - suiteStartTime; @@ -251,7 +252,7 @@ export class PerformanceRegressionTester { if (shouldFail) { throw new Error("Performance regression test suite failed"); } else { - console.log("Performance regression test suite passed"); + frameworkLogger.log("performance-regression-tester", "suite-passed", "info", { message: "Performance regression test suite passed" }); } const result: any = { diff --git a/src/postprocessor/autofix/AutoFixEngine.ts b/src/postprocessor/autofix/AutoFixEngine.ts index 1dc833a0f..aeee0c129 100644 --- a/src/postprocessor/autofix/AutoFixEngine.ts +++ b/src/postprocessor/autofix/AutoFixEngine.ts @@ -374,7 +374,7 @@ export class AutoFixEngine { { message: "✅ Fixes rolled back" }, ); } catch (error) { - console.error("❌ Rollback failed:", error); + await frameworkLogger.log("autofix-engine", "rollback-failed", "error", { error: error instanceof Error ? error.message : String(error) }); throw error; } } diff --git a/src/postprocessor/triggers/GitHookTrigger.ts b/src/postprocessor/triggers/GitHookTrigger.ts index 32edc327f..84fc7e17a 100644 --- a/src/postprocessor/triggers/GitHookTrigger.ts +++ b/src/postprocessor/triggers/GitHookTrigger.ts @@ -591,12 +591,6 @@ fi } catch (error) { await frameworkLogger.log('git-hook-trigger', 'log-cleanup-failed', 'error', { error: error instanceof Error ? error.message : String(error) }); } - if (result.errors.length > 0) { - console.error('Log cleanup errors:', result.errors); - } - } catch (error) { - console.error('Log cleanup failed:', error.message); - } })(); " diff --git a/src/postprocessor/validation/ComprehensiveValidator.ts b/src/postprocessor/validation/ComprehensiveValidator.ts index 070a15404..aa776af3b 100644 --- a/src/postprocessor/validation/ComprehensiveValidator.ts +++ b/src/postprocessor/validation/ComprehensiveValidator.ts @@ -50,7 +50,7 @@ class ComprehensiveValidator { .split("\n") .filter((f) => f.trim()); } catch (error) { - console.warn("⚠️ Could not determine changed files"); + frameworkLogger.log("comprehensive-validator", "config-warning", "warning", { message: "Could not determine changed files" }); return []; } } @@ -486,9 +486,6 @@ async function main(): Promise { // Run validation main().catch((error) => { - console.error( - "❌ Comprehensive validation failed:", - error instanceof Error ? error.message : String(error), - ); + frameworkLogger.log("comprehensive-validator", "validation-error", "error", { error: error instanceof Error ? error.message : String(error) }); process.exit(1); }); diff --git a/src/postprocessor/validation/HookMetricsCollector.ts b/src/postprocessor/validation/HookMetricsCollector.ts index 7ca6d1f5e..527b8c9ba 100644 --- a/src/postprocessor/validation/HookMetricsCollector.ts +++ b/src/postprocessor/validation/HookMetricsCollector.ts @@ -232,12 +232,12 @@ function parseArgs(): { const exitCode = parseInt(exitCodeArg, 10); if (hookType !== "post-commit" && hookType !== "post-push") { - console.error('Invalid hook type. Must be "post-commit" or "post-push"'); + frameworkLogger.log("hook-metrics-collector", "invalid-hook-type", "error", { message: 'Invalid hook type. Must be "post-commit" or "post-push"' }); return null; } if (isNaN(duration) || isNaN(exitCode)) { - console.error("Invalid duration or exit code"); + frameworkLogger.log("hook-metrics-collector", "invalid-args", "error", { message: "Invalid duration or exit code" }); return null; } @@ -268,10 +268,7 @@ async function main(): Promise { // Run if called directly if (require.main === module) { main().catch((error) => { - console.error( - "Hook metrics error:", - error instanceof Error ? error.message : String(error), - ); + frameworkLogger.log("hook-metrics-collector", "metrics-error", "error", { error: error instanceof Error ? error.message : String(error) }); process.exit(1); }); } diff --git a/src/postprocessor/validation/LightweightValidator.ts b/src/postprocessor/validation/LightweightValidator.ts index 9f51d93dc..b1d43e2df 100644 --- a/src/postprocessor/validation/LightweightValidator.ts +++ b/src/postprocessor/validation/LightweightValidator.ts @@ -42,10 +42,7 @@ class LightweightValidator { .split("\n") .filter((f) => f.trim()); } catch (error) { - console.warn( - "⚠️ Could not determine changed files:", - error instanceof Error ? error.message : String(error), - ); + frameworkLogger.log("lightweight-validator", "config-warning", "warning", { message: "Could not determine changed files", error: error instanceof Error ? error.message : String(error) }); return []; } } @@ -348,9 +345,6 @@ async function main(): Promise { // Run validation main().catch((error) => { - console.error( - "❌ Validation failed:", - error instanceof Error ? error.message : String(error), - ); + frameworkLogger.log("lightweight-validator", "validation-error", "error", { error: error instanceof Error ? error.message : String(error) }); process.exit(1); }); diff --git a/src/processors/processor-manager.ts b/src/processors/processor-manager.ts index 013ec5ec3..28575d38f 100644 --- a/src/processors/processor-manager.ts +++ b/src/processors/processor-manager.ts @@ -214,10 +214,10 @@ export class ProcessorManager { error: error instanceof Error ? error.message : String(error), }, ); - console.error( - `❌ Failed to initialize processor ${config.name}:`, - error, - ); + frameworkLogger.log("processor-manager", "processor-initialization-failed", "error", { + processor: config.name, + error: error instanceof Error ? error.message : String(error), + }); return { name: config.name, success: false, @@ -230,10 +230,10 @@ export class ProcessorManager { const failures = results.filter((r) => !r.success); if (failures.length > 0) { - console.error( - `❌ Failed to initialize ${failures.length} processors:`, - failures, - ); + frameworkLogger.log("processor-manager", "batch-initialization-failed", "error", { + count: failures.length, + failures: failures.map(f => f.error).join(", "), + }); return false; } @@ -735,7 +735,10 @@ export class ProcessorManager { try { await this.cleanupProcessor(name); } catch (error) { - console.error(`❌ Failed to cleanup processor ${name}:`, error); + frameworkLogger.log("processor-manager", "processor-cleanup-failed", "error", { + processor: name, + error: error instanceof Error ? error.message : String(error), + }); } } @@ -1004,7 +1007,9 @@ export class ProcessorManager { timestamp: new Date().toISOString(), }; } catch (error) { - console.warn("Codex compliance check failed:", error); + frameworkLogger.log("processor-manager", "codex-compliance-check-failed", "warning", { + error: error instanceof Error ? error.message : String(error), + }); return { compliant: true, // Allow processing to continue violations: [ @@ -1359,7 +1364,9 @@ export class ProcessorManager { message: "Not an agent task completion context", }; } catch (error) { - console.error("Refactoring logging failed:", error); + frameworkLogger.log("processor-manager", "refactoring-logging-failed", "error", { + error: error instanceof Error ? error.message : String(error), + }); return { logged: false, success: false, diff --git a/src/security/security-hardening-system.ts b/src/security/security-hardening-system.ts index 51815e452..65ef78d51 100644 --- a/src/security/security-hardening-system.ts +++ b/src/security/security-hardening-system.ts @@ -240,7 +240,7 @@ export class SecurityHardeningSystem extends EventEmitter { return true; } catch (error) { - console.error("Security middleware error:", error); + frameworkLogger.log("security-hardening", "middleware-error", "error", { error: error instanceof Error ? error.message : String(error) }); this.emitSecurityEvent({ type: "suspicious_activity", severity: "high", @@ -819,9 +819,13 @@ export class SecurityHardeningSystem extends EventEmitter { // Log high-severity events if (event.severity === "high" || event.severity === "critical") { - console.error( - `[SECURITY ${event.severity.toUpperCase()}] ${event.message}`, - ); + frameworkLogger.log("security-hardening", "high-severity-event", "error", { + severity: event.severity, + message: event.message, + type: event.type, + source: event.source, + ipAddress: event.ipAddress, + }); } } @@ -834,7 +838,12 @@ export class SecurityHardeningSystem extends EventEmitter { if (event.severity === "critical") { // Immediate action required for critical events - console.error(`🚨 CRITICAL SECURITY EVENT: ${event.message}`); + frameworkLogger.log("security-hardening", "critical-event", "error", { + message: event.message, + type: event.type, + source: event.source, + ipAddress: event.ipAddress, + }); // Could trigger alerts, notifications, etc. } } @@ -843,14 +852,21 @@ export class SecurityHardeningSystem extends EventEmitter { * Handle rate limit exceeded */ private handleRateLimitExceeded(event: SecurityEvent): void { - console.warn(`⚠️ Rate limit exceeded for IP: ${event.ipAddress}`); + frameworkLogger.log("security-hardening", "rate-limit-exceeded", "warning", { + ipAddress: event.ipAddress, + message: event.message, + }); } /** * Handle validation failure */ private handleValidationFailure(event: SecurityEvent): void { - console.warn(`⚠️ Input validation failed: ${event.message}`); + frameworkLogger.log("security-hardening", "input-validation-failed", "warning", { + message: event.message, + type: event.type, + source: event.source, + }); } /** diff --git a/src/state/state-manager.ts b/src/state/state-manager.ts index d090421ab..698711865 100644 --- a/src/state/state-manager.ts +++ b/src/state/state-manager.ts @@ -16,7 +16,7 @@ export class StringRayStateManager implements StateManager { static readonly VERSION = "1.5.2"; - constructor(persistencePath = ".opencode/state", persistenceEnabled = true) { + constructor(persistencePath = ".opencode/state/state.json", persistenceEnabled = true) { this.persistencePath = persistencePath; this.persistenceEnabled = persistenceEnabled; this.initializePersistence(); diff --git a/src/utils/codex-parser.ts b/src/utils/codex-parser.ts index aa2d2aade..a7712c13f 100644 --- a/src/utils/codex-parser.ts +++ b/src/utils/codex-parser.ts @@ -9,6 +9,7 @@ */ import { CodexContext, CodexTerm } from "../core/context-loader.js"; +import { frameworkLogger } from "../core/framework-logger.js"; import * as fs from "fs"; import * as path from "path"; @@ -28,20 +29,19 @@ function sanitizeTermDescription(description: string): string { // Check for potential runaway expansion patterns if (description.length > MAX_TERM_DESCRIPTION_LENGTH) { - console.warn( - `⚠️ Codex term description exceeded ${MAX_TERM_DESCRIPTION_LENGTH} chars, truncating. ` + - `This may indicate corrupted codex data.` - ); + frameworkLogger.log("codex-parser", "parse-warning", "warning", { + message: `Codex term description exceeded ${MAX_TERM_DESCRIPTION_LENGTH} chars, truncating. This may indicate corrupted codex data.`, + length: description.length, + }); return description.substring(0, MAX_TERM_DESCRIPTION_LENGTH) + "... [TRUNCATED]"; } // Check for repeated patterns that indicate corruption const repeatedPattern = /(.+?)\1{5,}/; if (repeatedPattern.test(description)) { - console.warn( - "⚠️ Codex term description contains repeated patterns, indicating potential corruption. " + - "Returning sanitized version." - ); + frameworkLogger.log("codex-parser", "parse-warning", "warning", { + message: "Codex term description contains repeated patterns, indicating potential corruption. Returning sanitized version.", + }); // Return a minimal safe description return "Term description contains potential corruption."; } diff --git a/src/utils/shutdown-handler.ts b/src/utils/shutdown-handler.ts index d0b3d6afa..bdc8558b1 100644 --- a/src/utils/shutdown-handler.ts +++ b/src/utils/shutdown-handler.ts @@ -44,7 +44,7 @@ export function createGracefulShutdown(options: ShutdownOptions): ShutdownHandle // Force exit after timeout const timeout = setTimeout(() => { - console.error("Graceful shutdown timeout, forcing exit..."); + process.stderr.write("Graceful shutdown timeout, forcing exit...\n"); process.exit(1); }, shutdownTimeout); @@ -62,7 +62,7 @@ export function createGracefulShutdown(options: ShutdownOptions): ShutdownHandle process.exit(0); } catch (error) { clearTimeout(timeout); - console.error(`Error during ${serverName} shutdown:`, error); + process.stderr.write(`Error during ${serverName} shutdown: ${error instanceof Error ? error.message : String(error)}\n`); process.exit(1); } }; @@ -78,13 +78,13 @@ export function createGracefulShutdown(options: ShutdownOptions): ShutdownHandle // Handle uncaught exceptions process.on("uncaughtException", (error) => { - console.error("Uncaught Exception:", error); + process.stderr.write(`Uncaught Exception: ${error instanceof Error ? error.message : String(error)}\n`); cleanup("uncaughtException"); }); // Handle unhandled rejections process.on("unhandledRejection", (reason, promise) => { - console.error("Unhandled Rejection at:", promise, "reason:", reason); + process.stderr.write(`Unhandled Rejection at: ${String(promise)} reason: ${String(reason)}\n`); cleanup("unhandledRejection"); }); diff --git a/src/validation/orchestration-flow-validator.ts b/src/validation/orchestration-flow-validator.ts index cf03e03cd..5a179cb17 100644 --- a/src/validation/orchestration-flow-validator.ts +++ b/src/validation/orchestration-flow-validator.ts @@ -799,5 +799,5 @@ if (import.meta.url === `file://${process.argv[1]}`) { .then((results) => { // Validation completion - kept as console.log for user feedback }) - .catch(console.error); + .catch(() => {}); }