@geraldmaron/construct 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +69 -0
- package/LICENSE +21 -0
- package/README.md +344 -0
- package/agents/prompts/cx-accessibility.md +29 -0
- package/agents/prompts/cx-ai-engineer.md +31 -0
- package/agents/prompts/cx-architect.md +40 -0
- package/agents/prompts/cx-business-strategist.md +25 -0
- package/agents/prompts/cx-data-analyst.md +30 -0
- package/agents/prompts/cx-data-engineer.md +32 -0
- package/agents/prompts/cx-debugger.md +27 -0
- package/agents/prompts/cx-designer.md +33 -0
- package/agents/prompts/cx-devil-advocate.md +36 -0
- package/agents/prompts/cx-docs-keeper.md +50 -0
- package/agents/prompts/cx-engineer.md +36 -0
- package/agents/prompts/cx-evaluator.md +26 -0
- package/agents/prompts/cx-explorer.md +38 -0
- package/agents/prompts/cx-legal-compliance.md +25 -0
- package/agents/prompts/cx-operations.md +28 -0
- package/agents/prompts/cx-orchestrator.md +30 -0
- package/agents/prompts/cx-platform-engineer.md +25 -0
- package/agents/prompts/cx-product-manager.md +30 -0
- package/agents/prompts/cx-qa.md +42 -0
- package/agents/prompts/cx-rd-lead.md +25 -0
- package/agents/prompts/cx-release-manager.md +31 -0
- package/agents/prompts/cx-researcher.md +31 -0
- package/agents/prompts/cx-reviewer.md +36 -0
- package/agents/prompts/cx-security.md +37 -0
- package/agents/prompts/cx-sre.md +26 -0
- package/agents/prompts/cx-test-automation.md +35 -0
- package/agents/prompts/cx-trace-reviewer.md +76 -0
- package/agents/prompts/cx-ux-researcher.md +30 -0
- package/agents/registry.json +879 -0
- package/agents/teams.json +94 -0
- package/bin/construct +613 -0
- package/commands/build/feature.md +21 -0
- package/commands/build/fix.md +20 -0
- package/commands/design/access.md +19 -0
- package/commands/design/flow.md +16 -0
- package/commands/design/ui.md +18 -0
- package/commands/measure/experiment.md +18 -0
- package/commands/measure/metrics.md +18 -0
- package/commands/measure/results.md +18 -0
- package/commands/plan/api.md +19 -0
- package/commands/plan/challenge.md +18 -0
- package/commands/plan/decide.md +20 -0
- package/commands/plan/feature.md +49 -0
- package/commands/plan/requirements.md +24 -0
- package/commands/remember/context.md +26 -0
- package/commands/remember/handoff.md +19 -0
- package/commands/remember/runbook.md +20 -0
- package/commands/review/code.md +21 -0
- package/commands/review/quality.md +19 -0
- package/commands/review/security.md +16 -0
- package/commands/ship/ready.md +19 -0
- package/commands/ship/release.md +19 -0
- package/commands/ship/status.md +18 -0
- package/commands/understand/docs.md +22 -0
- package/commands/understand/research.md +22 -0
- package/commands/understand/this.md +23 -0
- package/commands/understand/why.md +18 -0
- package/commands/work/clean.md +32 -0
- package/commands/work/drive.md +69 -0
- package/commands/work/optimize-prompts.md +46 -0
- package/commands/work/parallel-review.md +40 -0
- package/db/migrations/001_init.sql +38 -0
- package/langfuse/docker-compose.yml +82 -0
- package/lib/audit-skills.mjs +118 -0
- package/lib/auto-docs.mjs +293 -0
- package/lib/cli-commands.mjs +409 -0
- package/lib/codex-config.mjs +114 -0
- package/lib/comment-lint.mjs +191 -0
- package/lib/completions.mjs +170 -0
- package/lib/context-state.mjs +99 -0
- package/lib/cost.mjs +213 -0
- package/lib/distill.mjs +403 -0
- package/lib/efficiency.mjs +139 -0
- package/lib/env-config.mjs +53 -0
- package/lib/eval-harness.mjs +59 -0
- package/lib/features.mjs +209 -0
- package/lib/headhunt.mjs +597 -0
- package/lib/hooks/adaptive-lint.mjs +112 -0
- package/lib/hooks/agent-tracker.mjs +50 -0
- package/lib/hooks/bootstrap-guard.mjs +90 -0
- package/lib/hooks/comment-lint.mjs +26 -0
- package/lib/hooks/config-protection.mjs +52 -0
- package/lib/hooks/console-warn.mjs +43 -0
- package/lib/hooks/context-window-recovery.mjs +90 -0
- package/lib/hooks/continuation-enforcer.mjs +72 -0
- package/lib/hooks/dep-audit.mjs +155 -0
- package/lib/hooks/drive-guard.mjs +89 -0
- package/lib/hooks/edit-accumulator.mjs +36 -0
- package/lib/hooks/edit-error-recovery.mjs +46 -0
- package/lib/hooks/edit-guard.mjs +109 -0
- package/lib/hooks/env-check.mjs +80 -0
- package/lib/hooks/guard-bash.mjs +83 -0
- package/lib/hooks/mcp-audit.mjs +57 -0
- package/lib/hooks/mcp-health-check.mjs +51 -0
- package/lib/hooks/mcp-task-scope.mjs +47 -0
- package/lib/hooks/model-fallback.mjs +105 -0
- package/lib/hooks/pre-compact.mjs +212 -0
- package/lib/hooks/pre-push-gate.mjs +129 -0
- package/lib/hooks/read-tracker.mjs +129 -0
- package/lib/hooks/registry-sync.mjs +23 -0
- package/lib/hooks/scan-secrets.mjs +76 -0
- package/lib/hooks/session-start.mjs +95 -0
- package/lib/hooks/stop-notify.mjs +191 -0
- package/lib/hooks/stop-typecheck.mjs +83 -0
- package/lib/hooks/task-completed-guard.mjs +43 -0
- package/lib/hooks/teammate-idle-guard.mjs +54 -0
- package/lib/hooks/workflow-guard.mjs +62 -0
- package/lib/host-capabilities.mjs +123 -0
- package/lib/init-docs.mjs +382 -0
- package/lib/mcp/server.mjs +1123 -0
- package/lib/mcp-catalog.json +243 -0
- package/lib/mcp-manager.mjs +433 -0
- package/lib/mcp-platform-config.mjs +104 -0
- package/lib/model-router.mjs +810 -0
- package/lib/opencode-config.mjs +44 -0
- package/lib/opencode-runtime-plugin.mjs +909 -0
- package/lib/opencode-telemetry.mjs +433 -0
- package/lib/orchestration-policy.mjs +258 -0
- package/lib/prompt-composer.mjs +196 -0
- package/lib/prompt-metadata.mjs +68 -0
- package/lib/review.mjs +429 -0
- package/lib/role-preload.mjs +52 -0
- package/lib/schemas/decision.json +50 -0
- package/lib/schemas/implementation.json +51 -0
- package/lib/schemas/review-report.json +32 -0
- package/lib/schemas/test-report.json +43 -0
- package/lib/server/index.mjs +334 -0
- package/lib/server/static/app.js +841 -0
- package/lib/server/static/index.html +820 -0
- package/lib/service-manager.mjs +225 -0
- package/lib/setup.mjs +319 -0
- package/lib/status.mjs +707 -0
- package/lib/storage/backend.mjs +44 -0
- package/lib/storage/embeddings.mjs +70 -0
- package/lib/storage/hybrid-query.mjs +184 -0
- package/lib/storage/sql-store.mjs +55 -0
- package/lib/storage/state-source.mjs +101 -0
- package/lib/storage/sync.mjs +164 -0
- package/lib/storage/vector-store.mjs +59 -0
- package/lib/telemetry/backends/langfuse.mjs +58 -0
- package/lib/telemetry/backends/noop.mjs +16 -0
- package/lib/telemetry/langfuse-ingest.mjs +108 -0
- package/lib/telemetry/langfuse-model-sync.mjs +270 -0
- package/lib/telemetry/team-rollup.mjs +143 -0
- package/lib/toolkit-env.mjs +30 -0
- package/lib/validator.mjs +181 -0
- package/lib/workflow-state.mjs +794 -0
- package/package.json +53 -0
- package/personas/construct.md +94 -0
- package/platforms/claude/CLAUDE.md +30 -0
- package/platforms/claude/settings.template.json +472 -0
- package/platforms/opencode/config.template.json +65 -0
- package/platforms/opencode/plugins/construct-fallback.js +5 -0
- package/platforms/opencode/sync-config.mjs +69 -0
- package/rules/common/agents.md +55 -0
- package/rules/common/code-review.md +129 -0
- package/rules/common/coding-style.md +95 -0
- package/rules/common/comments.md +139 -0
- package/rules/common/cx-agent-routing.md +61 -0
- package/rules/common/cx-skill-routing.md +11 -0
- package/rules/common/development-workflow.md +49 -0
- package/rules/common/git-workflow.md +29 -0
- package/rules/common/hooks.md +35 -0
- package/rules/common/patterns.md +36 -0
- package/rules/common/performance.md +71 -0
- package/rules/common/security.md +34 -0
- package/rules/common/testing.md +62 -0
- package/rules/golang/coding-style.md +37 -0
- package/rules/golang/hooks.md +22 -0
- package/rules/golang/patterns.md +50 -0
- package/rules/golang/security.md +39 -0
- package/rules/golang/testing.md +36 -0
- package/rules/python/coding-style.md +47 -0
- package/rules/python/hooks.md +24 -0
- package/rules/python/patterns.md +44 -0
- package/rules/python/security.md +35 -0
- package/rules/python/testing.md +43 -0
- package/rules/swift/coding-style.md +52 -0
- package/rules/swift/hooks.md +25 -0
- package/rules/swift/patterns.md +71 -0
- package/rules/swift/security.md +38 -0
- package/rules/swift/testing.md +50 -0
- package/rules/typescript/coding-style.md +204 -0
- package/rules/typescript/hooks.md +27 -0
- package/rules/typescript/patterns.md +57 -0
- package/rules/typescript/security.md +33 -0
- package/rules/typescript/testing.md +23 -0
- package/rules/web/coding-style.md +101 -0
- package/rules/web/design-quality.md +68 -0
- package/rules/web/hooks.md +125 -0
- package/rules/web/patterns.md +84 -0
- package/rules/web/performance.md +69 -0
- package/rules/web/security.md +62 -0
- package/rules/web/testing.md +60 -0
- package/rules/zh/README.md +113 -0
- package/rules/zh/agents.md +55 -0
- package/rules/zh/code-review.md +129 -0
- package/rules/zh/coding-style.md +53 -0
- package/rules/zh/development-workflow.md +49 -0
- package/rules/zh/git-workflow.md +29 -0
- package/rules/zh/hooks.md +35 -0
- package/rules/zh/patterns.md +36 -0
- package/rules/zh/performance.md +60 -0
- package/rules/zh/security.md +34 -0
- package/rules/zh/testing.md +34 -0
- package/skills/ai/agent-dev.md +102 -0
- package/skills/ai/llm-security.md +105 -0
- package/skills/ai/ml-ops.md +169 -0
- package/skills/ai/orchestration-workflow.md +87 -0
- package/skills/ai/prompt-and-eval.md +114 -0
- package/skills/ai/prompt-optimizer.md +114 -0
- package/skills/ai/rag-system.md +104 -0
- package/skills/architecture/api-design.md +100 -0
- package/skills/architecture/caching.md +101 -0
- package/skills/architecture/cloud-native.md +97 -0
- package/skills/architecture/message-queue.md +100 -0
- package/skills/architecture/security-arch.md +105 -0
- package/skills/development/cpp.md +127 -0
- package/skills/development/go.md +129 -0
- package/skills/development/java.md +134 -0
- package/skills/development/kotlin.md +140 -0
- package/skills/development/mobile-crossplatform.md +144 -0
- package/skills/development/python.md +109 -0
- package/skills/development/rust.md +127 -0
- package/skills/development/shell.md +127 -0
- package/skills/development/swift.md +129 -0
- package/skills/development/typescript.md +129 -0
- package/skills/devops/ci-cd.md +108 -0
- package/skills/devops/containerization.md +106 -0
- package/skills/devops/cost-optimization.md +105 -0
- package/skills/devops/data-engineering.md +181 -0
- package/skills/devops/database.md +95 -0
- package/skills/devops/dependency-management.md +91 -0
- package/skills/devops/devsecops.md +96 -0
- package/skills/devops/git-workflow.md +100 -0
- package/skills/devops/incident-response.md +167 -0
- package/skills/devops/monorepo.md +87 -0
- package/skills/devops/observability.md +103 -0
- package/skills/devops/performance.md +104 -0
- package/skills/devops/testing.md +104 -0
- package/skills/docs/adr-workflow.md +32 -0
- package/skills/docs/backlog-proposal-workflow.md +19 -0
- package/skills/docs/customer-profile-workflow.md +22 -0
- package/skills/docs/evidence-ingest-workflow.md +23 -0
- package/skills/docs/init-docs.md +160 -0
- package/skills/docs/init-project.md +50 -0
- package/skills/docs/prd-workflow.md +57 -0
- package/skills/docs/prfaq-workflow.md +25 -0
- package/skills/docs/product-intelligence-review.md +22 -0
- package/skills/docs/product-intelligence-workflow.md +62 -0
- package/skills/docs/product-signal-workflow.md +27 -0
- package/skills/docs/research-workflow.md +28 -0
- package/skills/docs/runbook-workflow.md +24 -0
- package/skills/exploration/repo-map.md +297 -0
- package/skills/frameworks/django.md +159 -0
- package/skills/frameworks/nextjs.md +148 -0
- package/skills/frameworks/react.md +132 -0
- package/skills/frameworks/spring-boot.md +165 -0
- package/skills/frontend-design/accessibility.md +153 -0
- package/skills/frontend-design/component-patterns.md +111 -0
- package/skills/frontend-design/engineering.md +122 -0
- package/skills/frontend-design/state-management.md +118 -0
- package/skills/frontend-design/ui-aesthetics.md +102 -0
- package/skills/frontend-design/ux-principles.md +126 -0
- package/skills/quality-gates/review-work.md +90 -0
- package/skills/quality-gates/verify-change.md +94 -0
- package/skills/quality-gates/verify-module.md +96 -0
- package/skills/quality-gates/verify-quality.md +93 -0
- package/skills/quality-gates/verify-security.md +95 -0
- package/skills/roles/architect.ai-systems.md +37 -0
- package/skills/roles/architect.data.md +37 -0
- package/skills/roles/architect.enterprise.md +37 -0
- package/skills/roles/architect.integration.md +37 -0
- package/skills/roles/architect.md +68 -0
- package/skills/roles/architect.platform.md +37 -0
- package/skills/roles/data-analyst.experiment.md +37 -0
- package/skills/roles/data-analyst.md +68 -0
- package/skills/roles/data-analyst.product-intelligence.md +37 -0
- package/skills/roles/data-analyst.product.md +37 -0
- package/skills/roles/data-analyst.telemetry.md +37 -0
- package/skills/roles/data-engineer.pipeline.md +37 -0
- package/skills/roles/data-engineer.vector-retrieval.md +37 -0
- package/skills/roles/data-engineer.warehouse.md +37 -0
- package/skills/roles/debugger.md +68 -0
- package/skills/roles/designer.accessibility.md +43 -0
- package/skills/roles/designer.md +68 -0
- package/skills/roles/engineer.ai.md +49 -0
- package/skills/roles/engineer.data.md +49 -0
- package/skills/roles/engineer.md +85 -0
- package/skills/roles/engineer.platform.md +49 -0
- package/skills/roles/operator.docs.md +43 -0
- package/skills/roles/operator.md +68 -0
- package/skills/roles/operator.release.md +43 -0
- package/skills/roles/operator.sre.md +43 -0
- package/skills/roles/orchestrator.md +66 -0
- package/skills/roles/product-manager.ai-product.md +37 -0
- package/skills/roles/product-manager.business-strategy.md +43 -0
- package/skills/roles/product-manager.enterprise.md +37 -0
- package/skills/roles/product-manager.growth.md +37 -0
- package/skills/roles/product-manager.md +67 -0
- package/skills/roles/product-manager.platform.md +37 -0
- package/skills/roles/product-manager.product.md +37 -0
- package/skills/roles/qa.ai-eval.md +37 -0
- package/skills/roles/qa.api-contract.md +37 -0
- package/skills/roles/qa.data-pipeline.md +37 -0
- package/skills/roles/qa.md +68 -0
- package/skills/roles/qa.test-automation.md +49 -0
- package/skills/roles/qa.web-ui.md +37 -0
- package/skills/roles/researcher.explorer.md +43 -0
- package/skills/roles/researcher.md +68 -0
- package/skills/roles/researcher.ux.md +43 -0
- package/skills/roles/reviewer.devil-advocate.md +43 -0
- package/skills/roles/reviewer.evaluator.md +43 -0
- package/skills/roles/reviewer.md +68 -0
- package/skills/roles/reviewer.trace.md +43 -0
- package/skills/roles/security.ai.md +37 -0
- package/skills/roles/security.appsec.md +37 -0
- package/skills/roles/security.cloud.md +37 -0
- package/skills/roles/security.legal-compliance.md +49 -0
- package/skills/roles/security.md +68 -0
- package/skills/roles/security.privacy.md +37 -0
- package/skills/roles/security.supply-chain.md +37 -0
- package/skills/routing.md +139 -0
- package/skills/security/blue-team.md +83 -0
- package/skills/security/code-audit.md +94 -0
- package/skills/security/pentest.md +84 -0
- package/skills/security/red-team.md +81 -0
- package/skills/security/threat-intel.md +89 -0
- package/skills/security/vuln-research.md +87 -0
- package/skills/utility/clean-code.md +165 -0
- package/sync-agents.mjs +899 -0
- package/templates/docs/adr.md +27 -0
- package/templates/docs/backlog-proposal.md +26 -0
- package/templates/docs/customer-profile.md +34 -0
- package/templates/docs/evidence-brief.md +37 -0
- package/templates/docs/incident-report.md +46 -0
- package/templates/docs/memo.md +27 -0
- package/templates/docs/meta-prd.md +56 -0
- package/templates/docs/one-pager.md +24 -0
- package/templates/docs/prd-business.md +61 -0
- package/templates/docs/prd-platform.md +62 -0
- package/templates/docs/prd.md +47 -0
- package/templates/docs/prfaq.md +31 -0
- package/templates/docs/product-intelligence-report.md +31 -0
- package/templates/docs/research-brief.md +29 -0
- package/templates/docs/rfc-platform.md +60 -0
- package/templates/docs/rfc.md +46 -0
- package/templates/docs/runbook.md +33 -0
- package/templates/docs/signal-brief.md +25 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/opencode-telemetry.mjs — OpenCode → Langfuse full-fidelity tracing.
|
|
3
|
+
*
|
|
4
|
+
* Maintains per-session state, maps OpenCode events/hooks onto Langfuse
|
|
5
|
+
* trace/generation/span/event observations, and flushes on session.idle.
|
|
6
|
+
*/
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { createIngestClient } from "./telemetry/langfuse-ingest.mjs";
|
|
9
|
+
|
|
10
|
+
const SECRET_PATH_PATTERNS = [
|
|
11
|
+
/(^|\/)\.env(\.|$)/i,
|
|
12
|
+
/credentials/i,
|
|
13
|
+
/secrets?\./i,
|
|
14
|
+
/private.*key/i,
|
|
15
|
+
/\.pem$/i,
|
|
16
|
+
/id_rsa/i,
|
|
17
|
+
/id_ed25519/i,
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const LARGE_OUTPUT_CHARS = 20_000;
|
|
21
|
+
|
|
22
|
+
const sessionTraces = new Map();
|
|
23
|
+
let cachedIngest = null;
|
|
24
|
+
|
|
25
|
+
export function getIngestClient(env = process.env) {
|
|
26
|
+
if (cachedIngest) return cachedIngest;
|
|
27
|
+
cachedIngest = createIngestClient({
|
|
28
|
+
baseUrl: env.LANGFUSE_BASEURL,
|
|
29
|
+
publicKey: env.LANGFUSE_PUBLIC_KEY,
|
|
30
|
+
secretKey: env.LANGFUSE_SECRET_KEY,
|
|
31
|
+
onError: (err) => {
|
|
32
|
+
if (env.CONSTRUCT_TRACE_DEBUG === "1") {
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.error("[construct-telemetry]", err?.message || err);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
return cachedIngest;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function resetForTests() {
|
|
42
|
+
sessionTraces.clear();
|
|
43
|
+
cachedIngest = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function stringifyModel(model) {
|
|
47
|
+
if (!model) return undefined;
|
|
48
|
+
if (typeof model === "string") return model;
|
|
49
|
+
if (model.providerID && model.modelID) return `${model.providerID}/${model.modelID}`;
|
|
50
|
+
return model.id || model.name || undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function redactToolArgs(tool, args) {
|
|
54
|
+
if (!args || typeof args !== "object") return args;
|
|
55
|
+
const filePath = args.filePath || args.file_path || args.path;
|
|
56
|
+
if (typeof filePath === "string" && SECRET_PATH_PATTERNS.some((re) => re.test(filePath))) {
|
|
57
|
+
return { _redacted: "sensitive path", filePath };
|
|
58
|
+
}
|
|
59
|
+
return args;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function redactToolOutput(output) {
|
|
63
|
+
if (typeof output !== "string") return output;
|
|
64
|
+
if (output.length > LARGE_OUTPUT_CHARS) {
|
|
65
|
+
return `${output.slice(0, LARGE_OUTPUT_CHARS)}…[truncated ${output.length - LARGE_OUTPUT_CHARS} chars]`;
|
|
66
|
+
}
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function extractTextFromParts(parts) {
|
|
71
|
+
if (!Array.isArray(parts)) return "";
|
|
72
|
+
return parts
|
|
73
|
+
.filter((p) => p && typeof p === "object" && p.type === "text" && typeof p.text === "string")
|
|
74
|
+
.map((p) => p.text)
|
|
75
|
+
.join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getSessionIdFromEvent(event) {
|
|
79
|
+
return (
|
|
80
|
+
event?.properties?.info?.sessionID
|
|
81
|
+
|| event?.properties?.sessionID
|
|
82
|
+
|| event?.properties?.sessionId
|
|
83
|
+
|| event?.session?.id
|
|
84
|
+
|| event?.sessionID
|
|
85
|
+
|| event?.sessionId
|
|
86
|
+
|| event?.data?.sessionId
|
|
87
|
+
|| null
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getAgentFromEvent(event) {
|
|
92
|
+
return (
|
|
93
|
+
event?.properties?.info?.agent
|
|
94
|
+
|| event?.properties?.agent
|
|
95
|
+
|| event?.session?.agent
|
|
96
|
+
|| event?.agent
|
|
97
|
+
|| null
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function sanitizeUsage(tokens, cost) {
|
|
102
|
+
if (!tokens) return undefined;
|
|
103
|
+
const input = Number(tokens.input) || 0;
|
|
104
|
+
const output = Number(tokens.output) || 0;
|
|
105
|
+
const reasoning = Number(tokens.reasoning) || 0;
|
|
106
|
+
const total = input + output + reasoning;
|
|
107
|
+
if (!input && !output && !total) return undefined;
|
|
108
|
+
const usage = { input, output, total, unit: "TOKENS" };
|
|
109
|
+
if (Number(cost) > 0) usage.totalCost = Number(cost);
|
|
110
|
+
return usage;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function ensureSessionTrace(sessionId, { env = process.env, agent, userId, metadata } = {}) {
|
|
114
|
+
if (!sessionId) return null;
|
|
115
|
+
const ingest = getIngestClient(env);
|
|
116
|
+
if (!ingest.available) return null;
|
|
117
|
+
let entry = sessionTraces.get(sessionId);
|
|
118
|
+
if (!entry) {
|
|
119
|
+
const traceId = sessionId;
|
|
120
|
+
const startedAt = new Date().toISOString();
|
|
121
|
+
entry = {
|
|
122
|
+
traceId,
|
|
123
|
+
startedAt,
|
|
124
|
+
generationIdByMessage: new Map(),
|
|
125
|
+
toolSpanByCallId: new Map(),
|
|
126
|
+
pendingGenerationId: null,
|
|
127
|
+
agent: agent || null,
|
|
128
|
+
};
|
|
129
|
+
sessionTraces.set(sessionId, entry);
|
|
130
|
+
ingest.trace({
|
|
131
|
+
id: traceId,
|
|
132
|
+
name: agent ? `opencode.${agent}` : "opencode.session",
|
|
133
|
+
userId: userId || env.USER || env.USERNAME || undefined,
|
|
134
|
+
sessionId,
|
|
135
|
+
timestamp: startedAt,
|
|
136
|
+
tags: ["opencode", "construct", agent ? `agent:${agent}` : null].filter(Boolean),
|
|
137
|
+
metadata: metadata || undefined,
|
|
138
|
+
});
|
|
139
|
+
} else if (agent && !entry.agent) {
|
|
140
|
+
entry.agent = agent;
|
|
141
|
+
}
|
|
142
|
+
return entry;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Hook: chat.message ──────────────────────────────────────────────────────
|
|
146
|
+
export async function onChatMessage(input, output, { env = process.env } = {}) {
|
|
147
|
+
const ingest = getIngestClient(env);
|
|
148
|
+
if (!ingest.available) return;
|
|
149
|
+
const sessionId = input?.sessionID;
|
|
150
|
+
const entry = ensureSessionTrace(sessionId, { env, agent: input?.agent });
|
|
151
|
+
if (!entry) return;
|
|
152
|
+
const text = extractTextFromParts(output?.parts) || output?.message?.content;
|
|
153
|
+
ingest.event({
|
|
154
|
+
id: randomUUID(),
|
|
155
|
+
traceId: entry.traceId,
|
|
156
|
+
name: "user.message",
|
|
157
|
+
startTime: new Date().toISOString(),
|
|
158
|
+
input: { text, messageID: input?.messageID, variant: input?.variant },
|
|
159
|
+
metadata: {
|
|
160
|
+
agent: input?.agent,
|
|
161
|
+
model: stringifyModel(input?.model),
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Hook: chat.params ───────────────────────────────────────────────────────
|
|
167
|
+
export async function onChatParams(input, output, { env = process.env } = {}) {
|
|
168
|
+
const ingest = getIngestClient(env);
|
|
169
|
+
if (!ingest.available) return;
|
|
170
|
+
const sessionId = input?.sessionID;
|
|
171
|
+
const entry = ensureSessionTrace(sessionId, { env, agent: input?.agent });
|
|
172
|
+
if (!entry) return;
|
|
173
|
+
const generationId = randomUUID();
|
|
174
|
+
entry.pendingGenerationId = generationId;
|
|
175
|
+
ingest.generation({
|
|
176
|
+
id: generationId,
|
|
177
|
+
traceId: entry.traceId,
|
|
178
|
+
name: `llm.${input?.agent || "chat"}`,
|
|
179
|
+
startTime: new Date().toISOString(),
|
|
180
|
+
model: stringifyModel(input?.model),
|
|
181
|
+
modelParameters: {
|
|
182
|
+
temperature: output?.temperature,
|
|
183
|
+
topP: output?.topP,
|
|
184
|
+
topK: output?.topK,
|
|
185
|
+
maxOutputTokens: output?.maxOutputTokens,
|
|
186
|
+
},
|
|
187
|
+
input: input?.message,
|
|
188
|
+
metadata: {
|
|
189
|
+
agent: input?.agent,
|
|
190
|
+
provider: input?.provider?.info?.id || input?.model?.providerID,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── Hook: tool.execute.before ───────────────────────────────────────────────
|
|
196
|
+
export async function onToolBefore(input, output, { env = process.env } = {}) {
|
|
197
|
+
const ingest = getIngestClient(env);
|
|
198
|
+
if (!ingest.available) return;
|
|
199
|
+
const sessionId = input?.sessionID;
|
|
200
|
+
const entry = ensureSessionTrace(sessionId, { env });
|
|
201
|
+
if (!entry) return;
|
|
202
|
+
const spanId = randomUUID();
|
|
203
|
+
const startedAt = new Date().toISOString();
|
|
204
|
+
entry.toolSpanByCallId.set(input?.callID, { spanId, startedAt, tool: input?.tool });
|
|
205
|
+
ingest.span({
|
|
206
|
+
id: spanId,
|
|
207
|
+
traceId: entry.traceId,
|
|
208
|
+
name: `tool.${input?.tool}`,
|
|
209
|
+
startTime: startedAt,
|
|
210
|
+
input: redactToolArgs(input?.tool, output?.args),
|
|
211
|
+
metadata: { tool: input?.tool, callID: input?.callID },
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Hook: tool.execute.after ────────────────────────────────────────────────
|
|
216
|
+
export async function onToolAfter(input, output, { env = process.env } = {}) {
|
|
217
|
+
const ingest = getIngestClient(env);
|
|
218
|
+
if (!ingest.available) return;
|
|
219
|
+
const entry = sessionTraces.get(input?.sessionID);
|
|
220
|
+
const tracked = entry?.toolSpanByCallId.get(input?.callID);
|
|
221
|
+
if (!entry || !tracked) return;
|
|
222
|
+
ingest.spanUpdate({
|
|
223
|
+
id: tracked.spanId,
|
|
224
|
+
traceId: entry.traceId,
|
|
225
|
+
endTime: new Date().toISOString(),
|
|
226
|
+
output: redactToolOutput(output?.output),
|
|
227
|
+
metadata: {
|
|
228
|
+
tool: input?.tool,
|
|
229
|
+
title: output?.title,
|
|
230
|
+
...(output?.metadata && typeof output.metadata === "object" ? output.metadata : {}),
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
entry.toolSpanByCallId.delete(input?.callID);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── Hook: permission.ask ────────────────────────────────────────────────────
|
|
237
|
+
export async function onPermissionAsk(input, output, { env = process.env } = {}) {
|
|
238
|
+
const ingest = getIngestClient(env);
|
|
239
|
+
if (!ingest.available) return;
|
|
240
|
+
const sessionId = input?.sessionID || input?.session?.id;
|
|
241
|
+
const entry = ensureSessionTrace(sessionId, { env });
|
|
242
|
+
if (!entry) return;
|
|
243
|
+
ingest.event({
|
|
244
|
+
id: randomUUID(),
|
|
245
|
+
traceId: entry.traceId,
|
|
246
|
+
name: `permission.${output?.status || "ask"}`,
|
|
247
|
+
startTime: new Date().toISOString(),
|
|
248
|
+
input: {
|
|
249
|
+
type: input?.type,
|
|
250
|
+
title: input?.title,
|
|
251
|
+
pattern: input?.pattern,
|
|
252
|
+
tool: input?.tool,
|
|
253
|
+
},
|
|
254
|
+
metadata: { status: output?.status },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── Hook: command.execute.before ────────────────────────────────────────────
|
|
259
|
+
export async function onCommandBefore(input, output, { env = process.env } = {}) {
|
|
260
|
+
const ingest = getIngestClient(env);
|
|
261
|
+
if (!ingest.available) return;
|
|
262
|
+
const sessionId = input?.sessionID;
|
|
263
|
+
const entry = ensureSessionTrace(sessionId, { env });
|
|
264
|
+
if (!entry) return;
|
|
265
|
+
ingest.event({
|
|
266
|
+
id: randomUUID(),
|
|
267
|
+
traceId: entry.traceId,
|
|
268
|
+
name: `command.${input?.command}`,
|
|
269
|
+
startTime: new Date().toISOString(),
|
|
270
|
+
input: { command: input?.command, arguments: input?.arguments },
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Bus: event ──────────────────────────────────────────────────────────────
|
|
275
|
+
export async function onBusEvent(event, { env = process.env, buildUsage } = {}) {
|
|
276
|
+
const ingest = getIngestClient(env);
|
|
277
|
+
if (!ingest.available) return;
|
|
278
|
+
const sessionId = getSessionIdFromEvent(event);
|
|
279
|
+
if (!sessionId) return;
|
|
280
|
+
const agent = getAgentFromEvent(event);
|
|
281
|
+
const entry = ensureSessionTrace(sessionId, { env, agent });
|
|
282
|
+
if (!entry) return;
|
|
283
|
+
|
|
284
|
+
switch (event.type) {
|
|
285
|
+
case "message.updated": {
|
|
286
|
+
const info = event?.properties?.info;
|
|
287
|
+
if (!info || info.role !== "assistant") return;
|
|
288
|
+
if (!info.time?.completed) return;
|
|
289
|
+
const msgId = info.id;
|
|
290
|
+
const existingGenId = entry.generationIdByMessage.get(msgId);
|
|
291
|
+
const genId = existingGenId || entry.pendingGenerationId || randomUUID();
|
|
292
|
+
entry.generationIdByMessage.set(msgId, genId);
|
|
293
|
+
entry.pendingGenerationId = null;
|
|
294
|
+
|
|
295
|
+
const usage = typeof buildUsage === "function"
|
|
296
|
+
? buildUsage(event)
|
|
297
|
+
: sanitizeUsage(info.tokens, info.cost);
|
|
298
|
+
|
|
299
|
+
const outputText = extractTextFromParts(info.parts);
|
|
300
|
+
|
|
301
|
+
const body = {
|
|
302
|
+
id: genId,
|
|
303
|
+
traceId: entry.traceId,
|
|
304
|
+
name: `llm.${agent || "chat"}`,
|
|
305
|
+
startTime: info.time?.created ? new Date(info.time.created).toISOString() : undefined,
|
|
306
|
+
endTime: info.time?.completed ? new Date(info.time.completed).toISOString() : new Date().toISOString(),
|
|
307
|
+
model: info.modelID && info.providerID ? `${info.providerID}/${info.modelID}` : (info.modelID || undefined),
|
|
308
|
+
output: outputText || undefined,
|
|
309
|
+
usage: usage ? {
|
|
310
|
+
input: usage.input,
|
|
311
|
+
output: usage.output,
|
|
312
|
+
total: usage.total,
|
|
313
|
+
unit: usage.unit,
|
|
314
|
+
...(usage.totalCost ? { totalCost: usage.totalCost } : {}),
|
|
315
|
+
} : undefined,
|
|
316
|
+
metadata: {
|
|
317
|
+
agent,
|
|
318
|
+
provider: info.providerID,
|
|
319
|
+
...(info.tokens?.cache ? { cacheTokens: info.tokens.cache } : {}),
|
|
320
|
+
...(info.cost ? { costUsd: info.cost } : {}),
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
if (existingGenId) ingest.generationUpdate(body);
|
|
324
|
+
else ingest.generation(body);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
case "message.part.updated": {
|
|
329
|
+
const part = event?.properties?.part;
|
|
330
|
+
if (!part) return;
|
|
331
|
+
if (part.type === "reasoning" && part.text) {
|
|
332
|
+
ingest.event({
|
|
333
|
+
id: randomUUID(),
|
|
334
|
+
traceId: entry.traceId,
|
|
335
|
+
name: "assistant.reasoning",
|
|
336
|
+
startTime: new Date().toISOString(),
|
|
337
|
+
output: part.text,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
case "session.error": {
|
|
344
|
+
const err = event?.error || {};
|
|
345
|
+
ingest.event({
|
|
346
|
+
id: randomUUID(),
|
|
347
|
+
traceId: entry.traceId,
|
|
348
|
+
name: "session.error",
|
|
349
|
+
startTime: new Date().toISOString(),
|
|
350
|
+
level: "ERROR",
|
|
351
|
+
statusMessage: err.message || err.name || "session error",
|
|
352
|
+
metadata: { code: err.code, status: err.status, provider: err.provider },
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
case "session.idle":
|
|
358
|
+
case "session.compacted": {
|
|
359
|
+
ingest.event({
|
|
360
|
+
id: randomUUID(),
|
|
361
|
+
traceId: entry.traceId,
|
|
362
|
+
name: event.type,
|
|
363
|
+
startTime: new Date().toISOString(),
|
|
364
|
+
});
|
|
365
|
+
await ingest.flush();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
case "session.created":
|
|
370
|
+
case "session.updated": {
|
|
371
|
+
// Trace already created in ensureSessionTrace; record as event for timeline.
|
|
372
|
+
ingest.event({
|
|
373
|
+
id: randomUUID(),
|
|
374
|
+
traceId: entry.traceId,
|
|
375
|
+
name: event.type,
|
|
376
|
+
startTime: new Date().toISOString(),
|
|
377
|
+
});
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
case "file.edited":
|
|
382
|
+
case "EventFileEdited": {
|
|
383
|
+
ingest.event({
|
|
384
|
+
id: randomUUID(),
|
|
385
|
+
traceId: entry.traceId,
|
|
386
|
+
name: "file.edited",
|
|
387
|
+
startTime: new Date().toISOString(),
|
|
388
|
+
input: event?.properties || {},
|
|
389
|
+
});
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
case "todo.updated":
|
|
394
|
+
case "EventTodoUpdated": {
|
|
395
|
+
ingest.event({
|
|
396
|
+
id: randomUUID(),
|
|
397
|
+
traceId: entry.traceId,
|
|
398
|
+
name: "todo.updated",
|
|
399
|
+
startTime: new Date().toISOString(),
|
|
400
|
+
input: event?.properties || {},
|
|
401
|
+
});
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
case "command.executed":
|
|
406
|
+
case "EventCommandExecuted": {
|
|
407
|
+
ingest.event({
|
|
408
|
+
id: randomUUID(),
|
|
409
|
+
traceId: entry.traceId,
|
|
410
|
+
name: "command.executed",
|
|
411
|
+
startTime: new Date().toISOString(),
|
|
412
|
+
input: event?.properties || {},
|
|
413
|
+
});
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
case "permission.replied":
|
|
418
|
+
case "permission.updated":
|
|
419
|
+
case "permission.asked": {
|
|
420
|
+
ingest.event({
|
|
421
|
+
id: randomUUID(),
|
|
422
|
+
traceId: entry.traceId,
|
|
423
|
+
name: event.type,
|
|
424
|
+
startTime: new Date().toISOString(),
|
|
425
|
+
metadata: event?.properties || {},
|
|
426
|
+
});
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
default:
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/orchestration-policy.mjs — provider-agnostic routing and escalation policy.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const EXECUTION_TRACKS = {
|
|
6
|
+
immediate: 'immediate',
|
|
7
|
+
focused: 'focused',
|
|
8
|
+
orchestrated: 'orchestrated',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const INTENT_CLASSES = {
|
|
12
|
+
research: 'research',
|
|
13
|
+
implementation: 'implementation',
|
|
14
|
+
investigation: 'investigation',
|
|
15
|
+
evaluation: 'evaluation',
|
|
16
|
+
fix: 'fix',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const WORK_CATEGORIES = {
|
|
20
|
+
visual: 'visual',
|
|
21
|
+
deep: 'deep',
|
|
22
|
+
quick: 'quick',
|
|
23
|
+
writing: 'writing',
|
|
24
|
+
analysis: 'analysis',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const TERMINAL_STATES = ['DONE', 'BLOCKED', 'NEEDS_MAIN_INPUT'];
|
|
28
|
+
|
|
29
|
+
const SPECIALIST_MAP = {
|
|
30
|
+
implementation: ['cx-engineer'],
|
|
31
|
+
investigation: ['cx-debugger', 'cx-engineer'],
|
|
32
|
+
evaluation: ['cx-reviewer'],
|
|
33
|
+
fix: ['cx-debugger', 'cx-engineer'],
|
|
34
|
+
research: ['cx-researcher'],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function includesAny(text, patterns) {
|
|
38
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function containsAny(text, words) {
|
|
42
|
+
return words.some((word) => text.includes(word));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function isProductIntelligenceRequest(request = '') {
|
|
46
|
+
const text = String(request).toLowerCase();
|
|
47
|
+
return containsAny(text, [
|
|
48
|
+
'requirements',
|
|
49
|
+
'prd',
|
|
50
|
+
'prfaq',
|
|
51
|
+
'product brief',
|
|
52
|
+
'signal brief',
|
|
53
|
+
'customer notes',
|
|
54
|
+
'customer profile',
|
|
55
|
+
'product intelligence',
|
|
56
|
+
'backlog proposal',
|
|
57
|
+
'jira proposal',
|
|
58
|
+
'linear proposal',
|
|
59
|
+
'field notes',
|
|
60
|
+
'product spec',
|
|
61
|
+
'meta prd',
|
|
62
|
+
]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function classifyProductManagerFlavor(request = '') {
|
|
66
|
+
const text = String(request).toLowerCase();
|
|
67
|
+
if (containsAny(text, ['platform', 'api', 'sdk', 'developer experience', 'devex', 'integration', 'migration', 'compatibility', 'admin', 'tenant'])) return 'platform';
|
|
68
|
+
if (containsAny(text, ['enterprise', 'procurement', 'compliance', 'security review', 'audit', 'sso', 'soc2', 'soc 2', 'rbac'])) return 'enterprise';
|
|
69
|
+
if (containsAny(text, ['ai product', 'agent', 'eval', 'evaluation loop', 'model behavior', 'prompt', 'llm', 'human review'])) return 'ai-product';
|
|
70
|
+
if (containsAny(text, ['growth', 'activation', 'conversion', 'funnel', 'packaging', 'pricing', 'gtm', 'go-to-market'])) return 'growth';
|
|
71
|
+
return 'product';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function classifyArchitectFlavor(request = '') {
|
|
75
|
+
const text = String(request).toLowerCase();
|
|
76
|
+
if (containsAny(text, ['agent', 'rag', 'retrieval', 'embedding', 'eval loop', 'model behavior', 'tool use', 'llm'])) return 'ai-systems';
|
|
77
|
+
if (containsAny(text, ['integration', 'webhook', 'sync', 'third-party', 'oauth', 'reconciliation', 'idempotency'])) return 'integration';
|
|
78
|
+
if (containsAny(text, ['warehouse', 'schema', 'migration', 'retention', 'index', 'backfill', 'data model'])) return 'data';
|
|
79
|
+
if (containsAny(text, ['enterprise', 'sso', 'rbac', 'audit', 'data residency', 'procurement', 'tenant isolation'])) return 'enterprise';
|
|
80
|
+
if (containsAny(text, ['platform', 'api', 'sdk', 'developer experience', 'devex', 'admin', 'tenant', 'compatibility'])) return 'platform';
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function classifyQaFlavor(request = '') {
|
|
85
|
+
const text = String(request).toLowerCase();
|
|
86
|
+
if (containsAny(text, ['agent', 'prompt', 'model', 'eval', 'rag', 'retrieval', 'golden trace'])) return 'ai-eval';
|
|
87
|
+
if (containsAny(text, ['api', 'sdk', 'contract', 'status code', 'error body', 'openapi', 'consumer'])) return 'api-contract';
|
|
88
|
+
if (containsAny(text, ['pipeline', 'etl', 'elt', 'backfill', 'freshness', 'data quality', 'warehouse'])) return 'data-pipeline';
|
|
89
|
+
if (containsAny(text, ['ui', 'ux', 'screen', 'browser', 'playwright', 'responsive', 'keyboard', 'accessibility', 'visual'])) return 'web-ui';
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function classifySecurityFlavor(request = '') {
|
|
94
|
+
const text = String(request).toLowerCase();
|
|
95
|
+
if (containsAny(text, ['prompt injection', 'agent', 'rag', 'retrieval', 'embedding', 'model', 'tool scoping'])) return 'ai';
|
|
96
|
+
if (containsAny(text, ['privacy', 'pii', 'retention', 'deletion', 'consent', 'telemetry', 'trace', 'export'])) return 'privacy';
|
|
97
|
+
if (containsAny(text, ['dependency', 'package', 'supply chain', 'sbom', 'provenance', 'ci permission', 'signing'])) return 'supply-chain';
|
|
98
|
+
if (containsAny(text, ['cloud', 'iam', 'bucket', 'network policy', 'encryption', 'public access', 'drift'])) return 'cloud';
|
|
99
|
+
if (containsAny(text, ['auth', 'authorization', 'xss', 'csrf', 'ssrf', 'injection', 'input validation', 'jwt'])) return 'appsec';
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function classifyDataAnalystFlavor(request = '') {
|
|
104
|
+
const text = String(request).toLowerCase();
|
|
105
|
+
if (containsAny(text, ['customer signal', 'customer notes', 'evidence brief', 'product intelligence', 'field notes', 'signal brief'])) return 'product-intelligence';
|
|
106
|
+
if (containsAny(text, ['experiment', 'a/b', 'ab test', 'randomization', 'sample size', 'mde'])) return 'experiment';
|
|
107
|
+
if (containsAny(text, ['telemetry', 'trace', 'logs', 'dashboard', 'observability', 'denominator'])) return 'telemetry';
|
|
108
|
+
if (containsAny(text, ['metric', 'funnel', 'activation', 'adoption', 'retention', 'conversion', 'guardrail'])) return 'product';
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function classifyDataEngineerFlavor(request = '') {
|
|
113
|
+
const text = String(request).toLowerCase();
|
|
114
|
+
if (containsAny(text, ['vector', 'embedding', 'retrieval', 'hybrid search', 'semantic search', 'pgvector'])) return 'vector-retrieval';
|
|
115
|
+
if (containsAny(text, ['warehouse', 'metric layer', 'semantic layer', 'dimensional', 'partition', 'incremental model'])) return 'warehouse';
|
|
116
|
+
if (containsAny(text, ['pipeline', 'etl', 'elt', 'streaming', 'backfill', 'idempotent', 'data contract'])) return 'pipeline';
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function isDataAnalysisRequest(request = '') {
|
|
121
|
+
return Boolean(classifyDataAnalystFlavor(request));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function isDataEngineeringRequest(request = '') {
|
|
125
|
+
return Boolean(classifyDataEngineerFlavor(request));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function classifyRoleFlavors(request = '') {
|
|
129
|
+
return {
|
|
130
|
+
architect: classifyArchitectFlavor(request),
|
|
131
|
+
productManager: isProductIntelligenceRequest(request) ? classifyProductManagerFlavor(request) : null,
|
|
132
|
+
qa: classifyQaFlavor(request),
|
|
133
|
+
security: classifySecurityFlavor(request),
|
|
134
|
+
dataAnalyst: classifyDataAnalystFlavor(request),
|
|
135
|
+
dataEngineer: classifyDataEngineerFlavor(request),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function detectRiskFlags(request = '') {
|
|
140
|
+
const text = String(request).toLowerCase();
|
|
141
|
+
return {
|
|
142
|
+
architecture: containsAny(text, ['architecture', 'interface contract', 'api contract', 'dependency', 'module boundary', 'data model', 'indexing', 'retrieval design']),
|
|
143
|
+
security: containsAny(text, ['security', 'permission', 'secret', 'privacy', 'payment', 'authentication', 'authorization']),
|
|
144
|
+
dataIntegrity: containsAny(text, ['migration', 'data', 'sync', 'consistency', 'state']),
|
|
145
|
+
ui: containsAny(text, ['ui', 'ux', 'design system', 'screen', 'layout', 'visual', 'onboarding']) && !containsAny(text, ['requirements']),
|
|
146
|
+
docs: containsAny(text, ['docs', 'readme', 'runbook', 'adr']),
|
|
147
|
+
ai: containsAny(text, ['llm', ' agent', 'prompt', 'rag', 'model behavior', 'retrieval', 'embedding', 'vector']),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function classifyIntent(request = '') {
|
|
152
|
+
const text = String(request).toLowerCase();
|
|
153
|
+
if (containsAny(text, ['fix', 'bug', 'regression', 'broken', 'repair'])) return INTENT_CLASSES.fix;
|
|
154
|
+
if (includesAny(text, [/debug/, /investigate/, /trace/, /root cause/, /why .* fail/])) return INTENT_CLASSES.investigation;
|
|
155
|
+
if (isProductIntelligenceRequest(text)) return INTENT_CLASSES.implementation;
|
|
156
|
+
if (includesAny(text, [/review/, /audit/, /validate/, /ready to ship/, /check/])) return INTENT_CLASSES.evaluation;
|
|
157
|
+
if (includesAny(text, [/research/, /explore/, /compare/, /what does/, /explain/, /understand/, /docs?/])) return INTENT_CLASSES.research;
|
|
158
|
+
return INTENT_CLASSES.implementation;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function classifyWorkCategory(request = '', riskFlags = detectRiskFlags(request)) {
|
|
162
|
+
const text = String(request).toLowerCase();
|
|
163
|
+
if (riskFlags.ui) return WORK_CATEGORIES.visual;
|
|
164
|
+
if (riskFlags.docs || containsAny(text, ['write', 'rewrite', 'document', 'spec', 'requirements'])) return WORK_CATEGORIES.writing;
|
|
165
|
+
if (includesAny(text, [/analy[sz]e/, /measure/, /metrics/, /score/, /evaluate/])) return WORK_CATEGORIES.analysis;
|
|
166
|
+
if (riskFlags.architecture || riskFlags.ai || includesAny(text, [/plan/, /strategy/, /system/, /refactor/, /orchestr/])) return WORK_CATEGORIES.deep;
|
|
167
|
+
return WORK_CATEGORIES.quick;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function determineExecutionTrack({
|
|
171
|
+
request = '',
|
|
172
|
+
fileCount = 0,
|
|
173
|
+
moduleCount = 0,
|
|
174
|
+
introducesContract = false,
|
|
175
|
+
explicitDrive = false,
|
|
176
|
+
riskFlags = detectRiskFlags(request),
|
|
177
|
+
} = {}) {
|
|
178
|
+
const intent = classifyIntent(request);
|
|
179
|
+
if (explicitDrive) return EXECUTION_TRACKS.orchestrated;
|
|
180
|
+
if (intent === INTENT_CLASSES.research && fileCount <= 1 && moduleCount <= 1) return EXECUTION_TRACKS.immediate;
|
|
181
|
+
if (introducesContract || fileCount >= 3 || moduleCount >= 2) return EXECUTION_TRACKS.orchestrated;
|
|
182
|
+
if (riskFlags.architecture || riskFlags.security || riskFlags.dataIntegrity || riskFlags.ai) return EXECUTION_TRACKS.orchestrated;
|
|
183
|
+
if (fileCount <= 1 && moduleCount <= 1 && !includesAny(String(request).toLowerCase(), [/end to end/, /ship/, /full/])) return EXECUTION_TRACKS.immediate;
|
|
184
|
+
return EXECUTION_TRACKS.focused;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function selectSpecialists({ request = '', intent, track, riskFlags = detectRiskFlags(request) } = {}) {
|
|
188
|
+
const text = String(request).toLowerCase();
|
|
189
|
+
const productRequest = isProductIntelligenceRequest(text);
|
|
190
|
+
const dataAnalysisRequest = isDataAnalysisRequest(text);
|
|
191
|
+
const dataEngineeringRequest = isDataEngineeringRequest(text);
|
|
192
|
+
if (track === EXECUTION_TRACKS.immediate) return [];
|
|
193
|
+
if (track === EXECUTION_TRACKS.focused) {
|
|
194
|
+
if (riskFlags.ui) return ['cx-designer'];
|
|
195
|
+
if (productRequest) return ['cx-product-manager'];
|
|
196
|
+
if (dataEngineeringRequest) return ['cx-data-engineer'];
|
|
197
|
+
if (dataAnalysisRequest) return ['cx-data-analyst'];
|
|
198
|
+
if (riskFlags.docs) return ['cx-docs-keeper'];
|
|
199
|
+
if (riskFlags.security && intent === INTENT_CLASSES.evaluation) return ['cx-security'];
|
|
200
|
+
return SPECIALIST_MAP[intent] || ['cx-engineer'];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const specialists = ['cx-architect'];
|
|
204
|
+
if (intent === INTENT_CLASSES.fix || intent === INTENT_CLASSES.investigation) specialists.push('cx-debugger');
|
|
205
|
+
if (intent === INTENT_CLASSES.research) specialists.push('cx-researcher');
|
|
206
|
+
if (riskFlags.ui) specialists.push('cx-designer');
|
|
207
|
+
if (productRequest) specialists.push('cx-product-manager');
|
|
208
|
+
if (dataAnalysisRequest) specialists.push('cx-data-analyst');
|
|
209
|
+
if (dataEngineeringRequest) specialists.push('cx-data-engineer');
|
|
210
|
+
else if (riskFlags.docs) specialists.push('cx-docs-keeper');
|
|
211
|
+
specialists.push('cx-engineer', 'cx-reviewer', 'cx-qa');
|
|
212
|
+
if (riskFlags.security || riskFlags.dataIntegrity) specialists.push('cx-security');
|
|
213
|
+
return Array.from(new Set(specialists));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function requiresExecutiveApproval({
|
|
217
|
+
scopeChange = false,
|
|
218
|
+
productDecision = false,
|
|
219
|
+
riskAcceptance = false,
|
|
220
|
+
irreversibleAction = false,
|
|
221
|
+
blockedDependency = false,
|
|
222
|
+
} = {}) {
|
|
223
|
+
return Boolean(scopeChange || productDecision || riskAcceptance || irreversibleAction || blockedDependency);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function buildDispatchPlan({ track, intent, specialists = [] }) {
|
|
227
|
+
if (track === EXECUTION_TRACKS.immediate) return 'Plan: respond directly.';
|
|
228
|
+
if (track === EXECUTION_TRACKS.focused) return `Plan: ${specialists.join(' → ')}.`;
|
|
229
|
+
|
|
230
|
+
const phases = ['cx-architect'];
|
|
231
|
+
if (intent === INTENT_CLASSES.fix || intent === INTENT_CLASSES.investigation) phases.push('cx-debugger');
|
|
232
|
+
phases.push('cx-engineer');
|
|
233
|
+
const validators = specialists.filter((name) => ['cx-reviewer', 'cx-qa', 'cx-security'].includes(name));
|
|
234
|
+
if (validators.length) phases.push(validators.join(' + '));
|
|
235
|
+
return `Plan: ${phases.join(' → ')}.`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function routeRequest(options = {}) {
|
|
239
|
+
const intent = classifyIntent(options.request);
|
|
240
|
+
const riskFlags = detectRiskFlags(options.request);
|
|
241
|
+
const roleFlavors = classifyRoleFlavors(options.request);
|
|
242
|
+
const productFlavor = isProductIntelligenceRequest(options.request)
|
|
243
|
+
? classifyProductManagerFlavor(options.request)
|
|
244
|
+
: null;
|
|
245
|
+
const workCategory = classifyWorkCategory(options.request, riskFlags);
|
|
246
|
+
const track = determineExecutionTrack({ ...options, riskFlags });
|
|
247
|
+
const specialists = selectSpecialists({ ...options, intent, track, riskFlags });
|
|
248
|
+
return {
|
|
249
|
+
intent,
|
|
250
|
+
workCategory,
|
|
251
|
+
track,
|
|
252
|
+
riskFlags,
|
|
253
|
+
productFlavor,
|
|
254
|
+
roleFlavors,
|
|
255
|
+
specialists,
|
|
256
|
+
dispatchPlan: buildDispatchPlan({ track, intent, specialists }),
|
|
257
|
+
};
|
|
258
|
+
}
|